Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Switch User Stateless Bundle Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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.

Implementation Patterns

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.

Gotchas and Tips

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.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource