elvi/auth0-symfony
A maintained fork of auth0/symfony that adds up-to-date fixes and changes while including everything supported by the main package. Integrates Auth0 authentication into Symfony apps with a familiar, compatible setup.
Know what you're actually installing. elvi/auth0-symfony is a fork of auth0/symfony, not a Laravel package. There is no Auth0ServiceProvider, no publishable config/auth0.php, and no Auth0::fake() facade. If your application is Laravel-first, the day-to-day package you want is auth0/login (plus auth0/auth0-php for Management API work); reach for this fork only when you are working inside a Symfony service in the same estate, contributing a patch upstream, or deliberately borrowing a reference implementation of firewall/JWKS handling.
For a Symfony service that will actually run it:
composer require elvi/auth0-symfony
Then, in order of where to look first:
vendor/elvi/auth0-symfony/src/ — confirm the bundle class namespace before registering it (Elvi\Auth0Symfony\Auth0Bundle in most of these forks; it may still be Auth0\Symfony\Auth0Bundle if the fork was shallow). A composer show elvi/auth0-symfony and a quick rg "class .*Bundle" saves an hour.config/bundles.php — register it, then populate config/packages/auth0.yaml under the auth0: root key (sdk:, authenticator:, authorizer:). The fork inherits the same config tree as upstream, so upstream's docs are the authoritative reference.config/packages/security.yaml — wire the auth0_authenticator guard to your firewall and add the bundle's auth0 user provider. This is the first real use case: log a user in via Auth0 and get a Symfony\Component\Security\Core\User\UserInterface back in the token storage.Environment variables you'll set before anything boots:
AUTH0_DOMAIN=tenant.eu.auth0.com
AUTH0_CLIENT_ID=...
AUTH0_CLIENT_SECRET=...
AUTH0_COOKIE_SECRET=<32+ chars>
AUTH0_REDIRECT_URI=https://app.example.com/auth0/callback
AUTH0_MANAGEMENT_CLIENT_ID=...
AUTH0_MANAGEMENT_CLIENT_SECRET=...
The fastest sanity check is the bundle's built-in login/callback/logout routes — hit /auth0/login (or whatever prefix the fork registers) and confirm you come back authenticated. If that works, the SDK, session storage, and cookie handling are all correct and you can move on to your own controllers.
1. Treat the package as two SDKs, not one. Authentication (OIDC login, session, logout) and Management (user CRUD, roles, M2M) are configured separately and fail independently. Wire the Management API credentials as a separate M2M application in Auth0 with only the scopes you need — never reuse the web app's client secret for Management calls. Typical service definition:
services:
App\Service\Auth0Users:
arguments:
$management: '@auth0.management' # check the exact service id in the fork
If the fork doesn't expose a convenient service alias, instantiate Auth0\SDK\API\Management yourself from %env(AUTH0_DOMAIN)% and a cached token — it's a thin HTTP wrapper and you lose little.
2. Sync-on-login, not sync-on-callback. The common pattern is a UserProvider that lazily creates the local row the first time an Auth0 identity appears, keyed on sub. Keep sub in a unique, indexed column and store the connection name alongside it:
$user = $this->users->firstOrCreate(
['auth0_sub' => $auth0User->getId()],
['email' => $auth0User->getEmail(), 'name' => $auth0User->getName()]
);
This survives connection changes, avoids a webhook dependency, and means a user row exists exactly when someone can actually log in.
3. Borrow the pattern, not the code, into Laravel. The pieces of this fork worth replicating in a Laravel app are small and portable:
/.well-known/jwks.json, cache it under a key that includes the kid, and refresh on cache miss rather than on every request. auth0/auth0-php v8 does this for you; older flows in the Symfony SDK show the shape.cookie_secret; if you're proxying Auth0 sessions across a PHP monolith and a Symfony service, both need the same secret and compatible SameSite/Secure settings.middleware('auth0.jwt') that calls Auth0::decode($bearer, $tokenVerifier) and resolves sub to an Eloquent user — the Symfony equivalent is an AbstractAuthenticator, and the fork's implementation is a decent template.4. Machine-to-machine calls belong in a dedicated client. For service-to-service work, request a client_credentials token against https://{domain}/oauth/token with an audience and cache it until ~60s before expires_in. Do this in one class, not scattered through controllers, and give it its own Guzzle/Http instance with retry on 429.
5. Sharing config with Laravel. If a Laravel app and a Symfony service sit behind the same tenant, put the tenant config in environment variables with identical names and let each framework map them. Don't commit auth0.yaml values that differ from the Laravel side — divergence in redirect_uri or allowed callback URLs is the single most common cause of invalid_request / mismatching_state errors across a mixed stack.
6. Version-pin aggressively. This is a fork with 3 stars and no dependents; composer.json should pin the minor ("elvi/auth0-symfony": "~5.0") and your lockfile should be committed. If you need a newer Auth0 PHP SDK, you'll likely have to bump the fork's constraint yourself and open a PR — budget for that as maintenance, not as an incident.
It will not autowire in Laravel, and that's the point. No service provider, no config merge, no Facade. Any attempt to composer require this into a Laravel app and "just use it" produces a bundle class that never boots and config keys that are never read. Use auth0/login for the framework integration and auth0/auth0-php directly if you need lower-level control.
Fork drift is the real risk. The README's value proposition is "more up-to-date fixes than upstream," which is only true as long as someone is merging. Before adopting, diff against auth0/symfony at the same tag to see exactly what changed; if the diff is small, prefer upstream plus a patch file you control. Check whether the fork declares replace: {"auth0/symfony": "*"} — if not, having both installed produces two bundles fighting over the same service ids.
Management tokens expire. The Management API access token is valid for 24 hours; code that fetches one per request will hit /oauth/token rate limits under any real load. Cache it (Redis or the framework cache) with a TTL derived from expires_in, and never cache the token in the session.
Logout is a redirect, not a local operation. Clearing the local session leaves the Auth0 SSO cookie intact — the user is silently logged back in on the next /authorize. The Auth0-side logout requires a redirect to https://{domain}/v2/logout?client_id=...&returnTo=..., and returnTo must be in the tenant's Allowed Logout URLs or it is silently ignored. The bundle's logout controller handles this; hand-rolled logout is where most "logout doesn't work" tickets come from.
sub is connection-scoped. auth0|123 and google-oauth2|123 are different identities, and the same email can legitimately map to two sub values. A unique(email) constraint on your users table will cause a 500 the first time someone logs in with a second provider. Key on sub, allow duplicate emails, and handle linking deliberately.
JWKS staleness after key rotation. If you cache JWKS for a day and Auth0 rotates keys, tokens fail with signature verification failed for the remainder of the cache window. Cache indefinitely but key the entry by kid, so a miss forces a refetch. This is also the trick for debugging: a sudden wave of verification failures with no code deploy almost always means the cache wasn't kid-scoped.
Debug mode leaks tokens. The SDK's debug flag logs full tokens and session contents. If you enable it to chase a login problem, make sure it's env-gated (AUTH0_DEBUG=1 locally only) and never enabled in staging that shares logs with production.
Clock skew and azp/aud validation. The two most common verification failures in production are a 30-second clock drift between containers (fix: NTP, plus small leeway in the verifier) and a missing audience in the authorize request, producing an opaque ID token that downstream API middleware rejects. Log the decoded header (not the payload) when verification fails — it tells you immediately whether the token was even minted for you.
Extension points worth knowing. The bundle's user provider, the authenticator, and the session storage are all interface-backed and overridable via services.yaml; that's the correct place to inject your EntityManager-aware user factory rather than subclassing the bundle. For anything the bundle doesn't expose — custom claims, step-up auth, organization-scoped tokens — drop to auth0/auth0-php directly. The fork is a convenience layer, and fighting it is more expensive than bypassing it.
Verdict for a Laravel team: use this as a reading reference for how a mature framework integration wires Auth0 (firewall, JWKS cache, logout redirect, Management token lifecycle), and use auth0/login + auth0/auth0-php in your actual application. Adopt the fork itself only if you own the Symfony service it runs in and are prepared to maintain it.
How can I help you explore Laravel packages today?