The CWA is in heavy development
The CWA is still in alpha and not ready for production - some code and implementations are likely to change. If you would like to try out the CWA, please enjoy what we have provided and feel free to provide feedback, or get involved on GitHub.
DraftApi

Users & Security

JWT cookie authentication, the AbstractUser entity, email verification, password reset, and Symfony security configuration.

CWA uses cookie-based JWT authentication. The API sets a secure HttpOnly cookie on login; the Nuxt module reads auth state from the JWT payload. No Authorization headers, no manual token storage — the browser handles it transparently.

The Authentication Flow

  1. Client sends credentials to POST /login
  2. API validates, issues a JWT and a refresh token — both as HttpOnly cookies
  3. Every subsequent request sends the cookies automatically (same-origin or configured CORS)
  4. When the JWT expires, the API auto-refreshes it using the refresh token cookie
  5. The client never sees or stores the raw token values

AbstractUser

Your User entity (created by the Flex recipe) extends AbstractUser:

// src/Entity/User.php
namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Silverback\ApiComponentsBundle\Annotation as Silverback;
use Silverback\ApiComponentsBundle\Entity\User\AbstractUser;

#[ORM\Entity]
#[ApiResource(
    operations: [/* restrict to ROLE_SUPER_ADMIN */]
)]
class User extends AbstractUser
{
    // Add custom fields here
}

What AbstractUser Provides

FieldSerialization groupNotes
usernameUser:outputUsed as login identifier
emailAddressUser:outputUnique; separate from username
rolesUser:superAdminArray; ROLE_USER always included
enabledUser:superAdminDisabled users cannot log in
plainPasswordUser:input (write-only)Hashed before persist
emailAddressVerifiedUser:outputSet by email verification flow
newEmailAddressUser:outputPending email change

Passwords are never serialized to output — readable: false is set on the hashed password field.

Generating JWT Keys

One-time setup per environment:

mkdir -p config/jwt
openssl genpkey -out config/jwt/private.pem -aes256 -algorithm rsa -pkeyopt rsa_keygen_bits:4096
openssl pkey -in config/jwt/private.pem -out config/jwt/public.pem -pubout

Add to .env.local (never commit):

JWT_PASSPHRASE=your_secure_passphrase

Add to .env:

JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem

lexik_jwt_authentication Configuration

The JWT cookie name must match the refresh token cookie name in the bundle config:

# config/packages/lexik_jwt_authentication.yaml
lexik_jwt_authentication:
    secret_key: '%env(resolve:JWT_SECRET_KEY)%'
    public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
    pass_phrase: '%env(JWT_PASSPHRASE)%'
    token_ttl: 3600
    set_cookies:
        api_components:
            lifetime: 604800  # 1 week
            samesite: '%env(JWT_COOKIE_SAMESITE)%'
            secure: true
            httpOnly: true

Refresh Token Configuration

silverback_api_components:
    refresh_token:
        handler_id: silverback.api_components.refresh_token.storage.doctrine
        options:
            class: App\Entity\RefreshToken
        cookie_name: api_components   # must match lexik_jwt cookie name
        ttl: 604800                   # 1 week
        database_user_provider: database

Security Configuration

The Flex recipe provides a working security.yaml. Key sections:

security:
    role_hierarchy:
        ROLE_ADMIN: ROLE_USER
        ROLE_SUPER_ADMIN: ROLE_ADMIN

    firewalls:
        main:
            stateless: true
            provider: database
            guard:
                authenticators:
                    - lexik_jwt_authentication.jwt_token_authenticator

    access_control:
        # Anonymous access for auth flows
        - { path: '^/login', roles: PUBLIC_ACCESS }
        - { path: '^/token/refresh', roles: PUBLIC_ACCESS }
        - { path: '^/password/reset', roles: PUBLIC_ACCESS }
        - { path: '^/password/update', roles: PUBLIC_ACCESS }

        # Allow anonymous form submissions (contact forms etc.)
        - { path: '^/component/forms/.*/submit', methods: ['POST', 'PATCH'], roles: PUBLIC_ACCESS }

        # Public read access
        - { path: '^/', methods: ['GET'], roles: PUBLIC_ACCESS }

        # Mutations require authentication
        - { path: '^/', roles: ROLE_USER }

Email Verification Flow

Configure verification behaviour in silverback_api_components.yaml:

silverback_api_components:
    user:
        class_name: App\Entity\User
        email_verification:
            default_value: false          # new users start unverified
            verify_on_register: true      # send verification email on POST /users
            verify_on_change: true        # re-verify when email changes
            deny_unverified_login: true   # block unverified users from logging in
            email:
                redirect_path_query: null
                default_redirect_path: /verify-email/{{ username }}/{{ token }}
                subject: Please verify your email

The Nuxt module provides the /verify-email/[username]/[token] page automatically.

Password Reset Flow

silverback_api_components:
    user:
        password_reset:
            email:
                redirect_path_query: null
                default_redirect_path: /reset-password/{{ username }}/{{ token }}
                subject: Your password reset request
            repeat_ttl_seconds: 8600      # minimum time between reset requests
            request_timeout_seconds: 3600 # token validity window

The Nuxt module provides /forgot-password and /reset-password/[username]/[token] automatically.

Email Address Change Flow

silverback_api_components:
    user:
        new_email_confirmation:
            email:
                redirect_path_query: null
                default_redirect_path: /confirm-new-email/{{ username }}/{{ new_email }}/{{ token }}
                subject: Please confirm your new email address
            request_timeout_seconds: 86400

Notification Emails

Configure which system emails are sent and their subjects:

silverback_api_components:
    user:
        emails:
            welcome:
                enabled: true
                subject: 'Welcome to {{ website_name }}'
            user_enabled:
                enabled: true
                subject: 'Your account has been enabled'
            username_changed:
                enabled: true
                subject: 'Your username has been updated'
            password_changed:
                enabled: true
                subject: 'Your password has been changed'

Set MAILER_DSN in your environment:

MAILER_DSN=smtp://user:pass@smtp.example.com:587

Route Security

Restrict which routes are visible in the API based on the current user's role:

silverback_api_components:
    route_security:
        - { route: '/user-area*', security: "is_granted('ROLE_USER')" }
        - { route: '/admin*', security: "is_granted('ROLE_ADMIN')" }

route_security accepts any number of patterns. Each route value supports a * wildcard that matches any sequence of characters (internally converted to a regex — * does not need to be a %-style SQL wildcard). The security value is any Symfony expression-language security expression.

Two things happen for each rule:

  • Collection filtering — the Route collection endpoint (GET /_/routes) omits routes matching the pattern when the expression is not satisfied. Anonymous users won't see /admin/* routes at all.
  • Item access — fetching a specific Route by IRI is denied if any matching rule's expression fails.

routable_security

Controls who can read un-routed pages and page data, and who can create new pages and page data:

silverback_api_components:
    routable_security: "is_granted('ROLE_ADMIN')"

Without this, every Page and PageData record is visible in API collections regardless of whether it has a public route. This matters for template pages (isTemplate: true) and draft pages — they exist in the database but have no URL.

When routable_security is set:

GET (read access)

  • Users who pass the expression see all records (including un-routed ones).
  • Users who fail the expression only see records that have a route assigned — i.e., publicly accessible pages.

This prevents anonymous users from discovering admin-only templates or draft content through the collection endpoints.

POST (create access) Creating a new Page or PageData resource requires the routable_security expression to pass. This matches the existing restriction on edit operations — both creating and modifying CMS structure require admin access.

Component Security

Components are automatically secured based on the routes they are reachable through. You do not need to configure this — it is built into the bundle via ComponentVoter.

When a GET request is made for a specific component IRI, the bundle checks whether that component is accessible to the current user by:

  1. Route check — is the component part of a page that has a publicly accessible route? If yes, access is granted.
  2. PageData check — is the component referenced as a property on a PageData resource with a reachable route? If yes, access is granted.
  3. Template check — is the component placed in a page template used by reachable page data? If yes, access is granted.
  4. If none of the above resolve (the component is genuinely unreachable for this user), access is denied.

This means components placed exclusively on admin pages are automatically hidden from anonymous API clients without any extra configuration.

Creating the First Admin User

bin/console silverback:api-components:user:create

You'll be prompted for username, email, and password. Without flags, the user is created with ROLE_USER only.

FlagRole granted
(none)ROLE_USER
--adminROLE_ADMIN
--super-adminROLE_SUPER_ADMIN

To create an admin non-interactively:

bin/console silverback:api-components:user:create alice alice@example.com s3cr3t --admin