auth0/symfony
Symfony SDK for Auth0 authentication and Management APIs. Adds OAuth login, token validation, and user management to Symfony 6.4 LTS, 7, or 8 apps through a simple YAML config. Requires PHP 8.1+; install via Composer with auth0/symfony.
The SDK is a thin Symfony integration around the Auth0 PHP SDK: it gives you an authenticator (session/redirect flow), an authorizer (stateless bearer-token flow), a user provider, a handful of ready-made controllers, and a config tree. Almost all day-to-day work happens in three files: config/packages/auth0.yaml, .env.local, and config/packages/security.yaml.
1. Install and confirm the bundle is registered.
composer require auth0/symfony
Flex registers Auth0\Symfony\Auth0Bundle automatically. Check config/bundles.php; if the entry is missing, add Auth0\Symfony\Auth0Bundle::class => ['all' => true].
2. Create the Auth0 application in the dashboard. A Regular Web Application, with Token Endpoint Authentication Method set to POST. Add http://localhost:8000/callback to Allowed Callback URLs and http://localhost:8000/login to Allowed Logout URLs. Copy the domain, client ID, and client secret.
3. Wire the config. In config/packages/auth0.yaml, point everything at env vars (never hardcode secrets — this file gets committed), and use %kernel.secret% for cookie_secret:
auth0:
sdk:
domain: "%env(trim:string:AUTH0_DOMAIN)%"
client_id: "%env(trim:string:AUTH0_CLIENT_ID)%"
client_secret: "%env(trim:string:AUTH0_CLIENT_SECRET)%"
cookie_secret: "%kernel.secret%"
scopes: [openid, profile, email, offline_access]
Then the five route names under auth0.authenticator.routes (callback, success, failure, login, logout) — these are route names, not paths, and the SDK generates URLs from them.
4. Configure the security layer. In security.yaml, register the provider by service ID and add the two authenticators:
security:
providers:
auth0_provider:
id: Auth0\Symfony\Security\UserProvider
firewalls:
auth0:
pattern: ^/private$
provider: auth0_provider
custom_authenticators: [auth0.authenticator]
api:
pattern: ^/api
stateless: true
provider: auth0_provider
custom_authenticators: [auth0.authorizer]
dev: { pattern: ^/(_(profiler|wdt)|css|images|js)/, security: false }
main: { lazy: true }
5. Add the three helper routes in config/routes.yaml if you don't already have login/callback/logout endpoints — AuthenticationController::login, ::callback, ::logout give you a working flow with zero controller code.
First use case: a controller at /private that dumps $this->getUser(). Hit it unauthenticated and you get bounced to /login, round-tripped through Auth0, and dropped back on /private with a populated user object. That single loop validates domain, client secret, callback URL, cookie_secret, and route names all at once — if it works, your foundation is sound.
Two firewalls, two mental models. Keep them mentally separate from day one. The auth0.authenticator firewall is stateful: it stores an Auth0 session, redirects to the hosted login page, and $this->getUser() returns the SDK's user model. The auth0.authorizer firewall is stateless: it reads the Authorization: Bearer header, validates the JWT signature against the tenant's JWKS, and populates roles from scopes. A single app can (and usually does) run both — browser sessions on HTML routes, bearer tokens on <prefix>/api.
Scope-to-role mapping is the core authorization pattern. Scopes on the access token become ROLE_* constants: the scope read:messages becomes ROLE_READ_MESSAGES. So your access_control reads naturally:
access_control:
- { path: ^/api$, roles: PUBLIC_ACCESS }
- { path: ^/api/scoped$, roles: ROLE_USING_TOKEN } # any valid token
- { path: ^/api/scoped$, roles: ROLE_READ_MESSAGES } # token must carry read:messages
ROLE_USING_TOKEN is injected by the SDK on any request that carried a valid access token — useful as a coarse "authenticated at all" gate.
Post-login provisioning lives in a custom callback. The bundled AuthenticationController::callback is fine for prototypes, but most real apps need to create/update a local User row, assign app-specific roles, or log an audit event on first login. Point the callback route at your own controller instead: let the SDK finish the handshake, then read $this->getUser() (or the injected Auth0 service) and do your upserts before redirecting to the success route.
Swap the user provider for a Doctrine-backed one. If you need your own entity in $this->getUser(), extend Auth0\Symfony\Security\UserProvider and change the id: in security.yaml to your class. This is the cleanest extension point — you get the SDK's token handling plus your domain model, without reimplementing authentication.
Configure both caches in production. token_cache (JWKS lookups) and management_token_cache (Management API tokens) accept any PSR-6 pool. A Redis-backed pair is worth the ten minutes:
framework:
cache:
pools:
auth0_token_cache: { adapter: cache.adapter.redis }
auth0_management_token_cache: { adapter: cache.adapter.redis }
Without these, every request to a protected API route can trigger a JWKS fetch, and a busy Management API integration will hit rate limits fast.
Management API work. The bundle registers the underlying Auth0 PHP SDK services; use bin/console debug:container auth0 to discover the exact IDs for your version rather than guessing, then autowire by type. Typical tasks — listing users, reading/assigning roles, updating app metadata — go through that client. Always route it through management_token_cache and never call it in a loop per-request; batch or queue instead.
Backchannel logout requires one extra public route plus tenant configuration. Add a POST-only route to Auth0\Symfony\Controllers\BackchannelController::handle, mark it security: false (or PUBLIC_ACCESS), and point the tenant's Logout URI at it. It's only meaningful if you're using the SDK's session storage — it revokes sessions by sid.
Testing. The authenticators are container services, so unit tests can mock the Auth0 SDK service directly. For functional tests, a test firewall with security: false plus loginUser() on a real provider is usually simpler than trying to synthesize a valid JWT per test.
access_control is first-match-wins and order-sensitive. Symfony stops at the first matching rule. Put PUBLIC_ACCESS rules for genuinely open endpoints above the scoped ones, and specific paths above broad prefixes. A rule ordering mistake here silently produces 403s that look like token problems.AUTH0_ROUTE_* env values must match the route names in config/routes.yaml, not the URL paths. A mismatch throws RouteNotFoundException — and often only when someone clicks "login," not at boot. Verify with bin/console debug:router.localhost vs 127.0.0.1, a trailing slash, or http vs https will all fail with an opaque error from the tenant, not from Symfony. Check the Auth0 logs dashboard before debugging your PHP.POST. Defaults vary by application type; if it's not POST the client secret exchange fails at the callback step even though the redirect works.cookie_secret must be your kernel.secret, and it must be stable. It encrypts the SDK's cookie/session state. Rotating it logs everyone out; leaving it inconsistent between environments produces "state/nonce mismatch" errors that are tedious to diagnose./api with bearer tokens, every token's aud claim must match something in auth0.sdk.audiences. Requesting a token without the API audience gives you an opaque token that can never validate.cache.adapter.redis, cache.adapter.filesystem), not cache.app.contracts. Referencing a service ID that resolves to a contract adapter will fail at container-compile or first use.iss claim. Set custom_domain if you've configured one in the tenant, or every token validation will fail on issuer mismatch — usually only in production, which makes it a nasty deploy-day surprise.main firewall produces the classic redirect loop: Auth0 → callback → login → Auth0 → ...bin/console cache:clear after touching auth0.yaml or env values; otherwise you'll chase a bug that's actually a stale compiled container.bin/console debug:config auth0 (resolved config), debug:firewall <name> (authenticator list and access rules), and the Symfony profiler's Security panel (which authenticator ran, what roles were resolved). For JWT issues, decode the token at jwt.io and compare aud, iss, and scope against your config before touching code.offline_access scope is required for refresh tokens. Without it, the refresh_token is never issued and any silent-refresh logic you build will be dead code.How can I help you explore Laravel packages today?