auth0/jwt-auth-bundle
Symfony SDK for Auth0 authentication and Management APIs. Supports PHP 8.1+ and Symfony 6.4 LTS, 7, and 8. Install with Composer, then configure your Auth0 domain, Client ID, and Client Secret to add login, callback, and logout handling to your Symfony app.
Coming from Laravel, the first thing to internalise is that there is no Eloquent-shaped integration here. This bundle plugs into Symfony's Security component (firewalls, custom authenticators, a UserProvider), so the mental model to reach for is "guard driver + middleware", not "package I composer require and forget".
Minimal path to something running:
composer require auth0/symfony
config/packages/auth0.yaml first — the bundle's Flex recipe drops it there. It is the single source of truth for both halves of the package: auth0.sdk (domain, client credentials, audiences, scopes, caches) and auth0.authenticator (route names for callback/success/failure/login/logout).config/packages/security.yaml. This is where the abstraction lands and where your Laravel instincts mislead you: auth0.authenticator is the stateful (session) authenticator, auth0.authorizer is the stateless (bearer token) one. You register both against the same Auth0\Symfony\Security\UserProvider but on different firewalls.config/routes.yaml — AuthenticationController::login|callback|logout. This is the closest thing to plug-and-play; it gives you the full redirect → callback → token exchange → session loop for free.Your first real use case should be exactly this loop, not the Management API:
class ExampleController extends AbstractController
{
public function private(): Response
{
return $this->render('private.html.twig', ['user' => $this->getUser()]);
}
}
If /private renders a user object and /logout bounces you to the public page, the stateful half is done. Only then add the stateless API firewall. Trying to get both working simultaneously on day one is where most of the time gets lost.
Where to look when the README runs out: the README is genuinely good on configuration and thin on runtime behaviour. Budget an hour with vendor/auth0/symfony/src/ and bin/console debug:container auth0 --show-private. The docs/BackchannelLogout.md doc is the only non-README documentation in the repo, which tells you how much you should expect to self-serve.
Dual-firewall split. The idiomatic pattern is one session firewall for HTML routes and one stateless: true firewall for API prefixes:
firewalls:
auth0:
pattern: ^/private$
provider: auth0_provider
custom_authenticators: [auth0.authenticator]
api:
pattern: ^/api
stateless: true
provider: auth0_provider
custom_authenticators: [auth0.authorizer]
Keep the main firewall last and lazy: true. Symfony matches firewalls in declaration order; a greedy main will silently shadow your API firewall and you will spend an afternoon wondering why bearer tokens are ignored.
Scope-as-role mapping. This is the single best idea in the package and worth stealing even if you never ship Symfony. The authorizer translates the token's scope claim into Symfony roles named ROLE_<UPPERCASED_SCOPE>, so read:messages becomes ROLE_READ_MESSAGES. You then do authorization declaratively:
access_control:
- { path: ^/api$, roles: PUBLIC_ACCESS }
- { path: ^/api/scoped$, roles: ROLE_USING_TOKEN }
- { path: ^/api/scoped$, roles: ROLE_READ_MESSAGES }
In a Laravel codebase the equivalent is a middleware that maps scopes to Gate::define abilities, or a token.can:read:messages middleware. Two things to copy: the reserved ROLE_USING_TOKEN (set for any request carrying a valid access token, distinct from "has scope X") and the ordering convention — authentication check first, authorization check second.
Route names, not paths, in config. AUTH0_ROUTE_CALLBACK=callback refers to the route name in routes.yaml, not the URL. Day-to-day this means adding a route is a two-file change, and the failure mode for a typo is a runtime redirect to a 404, not a boot error. Add a smoke test that asserts $router->generate($configuredName) doesn't throw for each of the five configured routes.
Cache pools as services. token_cache and management_token_cache accept any PSR-6 pool service ID. Configure them on day one, not "later":
framework:
cache:
pools:
auth0_token_cache: { adapter: cache.adapter.redis }
auth0_management_token_cache: { adapter: cache.adapter.redis }
The token cache holds JWKS results. Without it, every token validation risks a network round trip to /jwks.json, and under load that endpoint will rate-limit you. In a Laravel port, this maps directly onto a Cache::remember() around the JWKS fetch with a long TTL.
Backchannel logout as a revocation listener. From 5.2 the package ships BackchannelController::handle, a public POST route. The pattern is: Auth0 POSTs a logout token, you invalidate that user's sessions. Note this is not the same as front-channel /logout, which only clears the local session — the user's Auth0 session stays alive. If "log out everywhere" matters, you need both, plus a session store you can enumerate (which Symfony's native session gives you only if you're on a shared backend).
Management API. The bundle wires the Auth0 PHP SDK underneath; you get the management client and its token cache. Reach for it for user CRUD, role assignment, and password-change tickets. But be honest about scope: if the Laravel app only ever needs to validate tokens, you do not need this bundle at all — use auth0/auth0-php and a small middleware. This package earns its keep only when you're doing bidirectional work (auth and management) inside Symfony's security model.
cookie_secret should be %kernel.secret%. It encrypts the session cookie payload. Rotating APP_SECRET — which people do casually — logs every user out and can break in-flight callbacks. Treat it as a production credential, not a throwaway.POST on the Auth0 application. The README says it once; if it's client_secret_basic the callback exchange fails with a vague 401 and nothing logs the cause. Check this before you touch code.audiences is not optional if you protect an API. Without it, the authorizer has no expected audience to compare against and either rejects valid tokens or accepts ones minted for a different API. Set AUTH0_API_AUDIENCE.CacheInterface. Passing a cache contract service ID fails at container build, not at runtime — at least that one fails loudly.^5.7 and don't let a composer update drift you onto an unsupported pairing. The support table stops at 5.7; anything older is EOL and won't get patches.AUTH0_ROUTE_* values are resolved at redirect time. Verify with bin/console debug:router and a boot-time assertion in a test.offline_access implies refresh tokens, which means the tenant's refresh token rotation and reuse settings become part of your auth story. If rotation is off, refresh tokens never expire and revocation depends entirely on your caching layer. If it's on and your cache is cold/stale, you get intermittent silent logins failing./logout does not revoke the Auth0 session. Users can be silently re-authenticated by the tenant's SSO cookie right after "logging out". If that surprises your product team, you need a federated logout redirect, not just the SDK's session clear.security.yaml is order-sensitive and misconfiguration produces "why am I anonymous here?" rather than an error. bin/console debug:firewall <name> is the tool.UserProvider is the seam. Injecting your own user repository behind it (rather than using the default profile-array user) is how you get $this->getUser() to return a Doctrine entity. Do this before you write controllers, not after.How can I help you explore Laravel packages today?