lafourchette/switch-user-stateless-bundle
A Symfony bundle for stateless user switching. Impersonate or switch between users without relying on session state, making it useful for debugging, testing, and admin tooling that integrates with Symfony's security system.
Install and register the bundle
composer require lafourchette/switch-user-stateless-bundle
Register it the way your Symfony version expects:
// Symfony 3/4 AppKernel.php
$bundles[] = new Lafourchette\SwitchUserStatelessBundle\SwitchUserStatelessBundle();
// Symfony 4+/5+ config/bundles.php
return [
// ...
Lafourchette\SwitchUserStatelessBundle\SwitchUserStatelessBundle::class => ['all' => true],
];
Wire it into a stateless firewall
The bundle exists because Symfony's native switch_user listener is session-oriented and does not do anything useful on a stateless: true firewall. Add the bundle's listener to the API firewall that already authenticates your admin users:
# config/packages/security.yaml
security:
firewalls:
api:
pattern: ^/api
stateless: true
# your normal authenticator (JWT, API key, custom guard...)
guard:
authenticators:
- App\Security\JwtAuthenticator
switch_user_stateless: ~
Check the bundle's DependencyInjection/Configuration.php for the exact config keys and defaults (provider name, role, header/query parameter). The package is small and thinly documented; read the listener first — it is the whole feature.
First use case
A support engineer holds a normal admin token but needs to see the API as a customer. They send their own credentials plus a switch header:
curl https://api.example.com/api/orders \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "X-Switch-User: customer-42"
The request is authenticated as customer-42 for that single call, without changing the admin's token and without any session state.
Per-request impersonation
Because the firewall is stateless, there is no "switch in / switch out" lifecycle. Every request must carry the switch header explicitly; the moment the client stops sending it, it is back to the original identity. Design your admin tooling around this: keep the acting-as identity in the admin UI and attach the header on each outgoing call.
Keep the gate role narrow
Only principals with the configured switch role (typically ROLE_ALLOWED_TO_SWITCH) should be able to impersonate. Grant that role through a dedicated support/admin role rather than reusing a broad admin role, and combine with route-level access control:
security:
role_hierarchy:
ROLE_SUPPORT: [ROLE_ALLOWED_TO_SWITCH, ROLE_API_READ]
Scope the firewall / environment
Put the switch-enabled firewall on the smallest useful pattern, and preferably enable it only where support tooling lives (or only outside production):
api_support:
pattern: ^/api/support
stateless: true
switch_user_stateless: ~
Audit the switch
Impersonation is a privileged action; log it. If the bundle dispatches a switch event, subscribe to it. If not, add a small kernel/security listener that reads the switch header and the authenticated token and records (actor, target, route, time). In a stateless API this fires per request, so consider logging only on non-GET requests or sampling read traffic to avoid flooding logs.
Use it in integration tests
The header is the entire contract, which makes it easy to test authorization boundaries:
$client->request('GET', '/api/orders', [], [], [
'HTTP_AUTHORIZATION' => 'Bearer '.$adminToken,
'HTTP_X_SWITCH_USER' => 'customer-42',
]);
$this->assertResponseIsSuccessful();
Assert the negative cases too: a user without the switch role gets 403, an unknown target gets a 4xx, and no header means no impersonation.
Combining with JWT
The switch happens in the firewall, not in your token. If downstream services trust JWT claims (sub, roles), they will still see the admin's claims. If you need the impersonated identity propagated, either terminate impersonation at the edge (API gateway) or mint a short-lived token for the target and record the actor separately (an OAuth-style act/may_act claim is the cleaner long-term design).
Programmatic switching
Inside the application (console commands, fixtures, internal service calls), construct a request with the switch header and run it through the kernel, or invoke the bundle's listener directly if it is exposed as a service. This keeps one code path for "act as user" rather than duplicating authentication logic.
Header spoofing is the main risk. If the switch header reaches the API from an untrusted network, anyone who can obtain an allowed role can impersonate anyone. Strip X-Switch-User (and the equivalent query parameter) at the edge/proxy for all external traffic, and only allow it on internal or support-only routes. Treat it like an admin-only header, not a client feature.
No exit step. There is no session to clear and no "switch back" endpoint. If your client caches or reuses headers, verify it is not leaking the impersonation header into subsequent requests.
Listener ordering matters. The switch listener must run after your authenticator has populated the token; if your JWT authenticator rejects the request first, impersonation never happens. If switching appears to be ignored, check firewall ordering, the listener priority, and whether the firewall actually matches the request (debug:firewall / debug:event-dispatcher).
The user provider must resolve the target. If you have multiple providers, point the bundle's config at the right one. An unknown target should produce a clean 4xx, not a 500 — test this.
Low-maintenance dependency. The bundle is small, has no visible community around it, and may pin older Symfony versions. Check composer.json constraints before pulling it into a modern Symfony app. If it does not fit, the behavior is small enough (read the header, check the role, replace the token) to reimplement as a custom listener in an afternoon — that is often the safer call for a production system.
Proxies and CORS. Load balancers and API gateways commonly strip unknown headers; confirm the switch header survives the hop. If a browser calls the API directly, add it to Access-Control-Allow-Headers and handle the preflight.
Response caching. Any response that varies by impersonated identity must send Vary: X-Switch-User (or be excluded from shared caches). Otherwise a CDN can serve one user's data to another.
Debugging checklist: bin/console debug:container to confirm the listener service is registered on the firewall, debug:event-dispatcher to see security listener order, and a temporary controller that dumps $this->getUser() plus security.token_storage to verify which identity actually reached the application. Log the raw switch header in dev to catch typos in the target username.
Extension points. Prefer replacing the bundle's listener service with your own subclass/decorator if you need custom provider resolution, audit hooks, or different header handling; and use a dedicated switch event/subscriber rather than scattering impersonation checks through controllers.
How can I help you explore Laravel packages today?