bigz/switch-user-stateless-bundle
## Getting Started
### Minimal Setup
1. **Installation**:
```bash
composer require lafourchette/switch-user-stateless-bundle
Register the bundle in config/bundles.php (Symfony Flex) or AppKernel.php (legacy):
LaFourchette\SwitchUserStatelessBundle\SwitchUserStatelessBundle::class => ['all' => true],
Configuration:
Add to config/packages/switch_user_stateless.yaml:
switch_user_stateless:
firewall: api # Target your API firewall
token_param: impersonate_token # Customize token param name
user_resolver: App\Service\UserResolver # Optional custom resolver
First Use Case: Trigger impersonation via a request header or query param:
curl -H "X-Impersonate-Token: YOUR_TOKEN" http://api.example.com/endpoint
Or via query string:
curl "http://api.example.com/endpoint?impersonate_token=YOUR_TOKEN"
Token Generation: Generate tokens for users in your admin panel or CLI:
use LaFourchette\SwitchUserStatelessBundle\Generator\TokenGenerator;
$generator = new TokenGenerator();
$token = $generator->generateToken($user); // Returns a unique token
Admin Requests Impersonation:
// Admin calls API with token
$response = $client->request('GET', '/api/orders', [
'headers' => ['X-Impersonate-Token' => $token]
]);
Middleware Handles Token: The bundle automatically validates the token and switches the user context.
Cleanup: Impersonation is stateless—no manual logout needed. Tokens expire after use (configurable).
Override the default resolver to fetch users from a custom source (e.g., database with soft-deletes):
// src/Service/CustomUserResolver.php
use LaFourchette\SwitchUserStatelessBundle\Resolver\UserResolverInterface;
class CustomUserResolver implements UserResolverInterface
{
public function findUserByToken($token)
{
return User::withTrashed()->where('api_token', $token)->first();
}
}
Register in config:
switch_user_stateless:
user_resolver: App\Service\CustomUserResolver
$token = $this->get('switch_user_stateless.token_generator')->generateToken($user);
TokenRevocator service to invalidate tokens (e.g., on user logout):
$this->get('switch_user_stateless.token_revocator')->revokeToken($token);
Use with API Platform’s ContextBuilder to pass the impersonated user to serializers:
// src/EventListener/SwitchUserListener.php
use LaFourchette\SwitchUserStatelessBundle\Event\SwitchUserEvent;
class SwitchUserListener
{
public function onSwitchUser(SwitchUserEvent $event)
{
$event->getContextBuilder()->setUser($event->getUser());
}
}
Register as a subscriber:
services:
App\EventListener\SwitchUserListener:
tags:
- { name: kernel.event_subscriber }
Combine with Symfony’s RateLimiter to restrict impersonation attempts:
# config/packages/security.yaml
firewalls:
api:
rate_limiter: impersonation_limiter
// config/services.yaml
services:
App\Security\ImpersonationRateLimiter:
arguments:
$limit: 5
$interval: '1 minute'
Token Leakage:
generateToken() are not stored; they must be manually persisted (e.g., in a user_api_tokens table).// src/Service/DatabaseTokenGenerator.php
class DatabaseTokenGenerator extends TokenGenerator
{
public function generateToken(User $user)
{
$token = parent::generateToken($user);
$user->apiTokens()->create(['token' => $token]);
return $token;
}
}
Firewall Mismatch:
firewall config doesn’t match your API’s actual firewall name, impersonation silently fails.config/packages/security.yaml:
firewalls:
api: # <-- Must match switch_user_stateless.firewall
pattern: ^/api
Token Expiration:
TokenRevocator to clear tokens after use:
// src/Service/TokenRevocator.php
class TokenRevocator
{
public function revokeToken($token)
{
// Logic to mark token as used/expired
}
}
Circular Dependencies:
UserResolver depends on services that require the user to be loaded (e.g., UserProvider), you’ll hit a circular dependency.$user = $this->userResolver->findUserByToken($token);
if (!$user) {
throw new \RuntimeException('Invalid token');
}
Enable Debugging:
Add this to config/packages/dev/switch_user_stateless.yaml:
switch_user_stateless:
debug: true # Logs impersonation events to Symfony's logger
Check Events:
Subscribe to SwitchUserEvent to debug impersonation:
use LaFourchette\SwitchUserStatelessBundle\Event\SwitchUserEvent;
class DebugSubscriber
{
public function onSwitchUser(SwitchUserEvent $event)
{
$this->logger->info('Switched to user', [
'original_user' => $event->getOriginalUser(),
'impersonated_user' => $event->getUser(),
]);
}
}
Token Validation:
Override TokenValidator to add custom logic:
// src/Validator/CustomTokenValidator.php
use LaFourchette\SwitchUserStatelessBundle\Validator\TokenValidatorInterface;
class CustomTokenValidator implements TokenValidatorInterface
{
public function validate($token)
{
if (strpos($token, 'INVALID_') === 0) {
throw new \RuntimeException('Invalid token prefix');
}
return true;
}
}
Register in config:
switch_user_stateless:
token_validator: App\Validator\CustomTokenValidator
Custom Token Storage:
Replace the default TokenGenerator to store tokens in Redis or a dedicated table:
// src/Generator/RedisTokenGenerator.php
class RedisTokenGenerator extends TokenGenerator
{
public function generateToken(User $user)
{
$token = parent::generateToken($user);
$this->redis->set("impersonation:$token", $user->id, 'EX', 3600);
return $token;
}
}
Event-Driven Workflows: Extend the bundle’s events to trigger actions (e.g., audit logs):
// Subscribe to SwitchUserEvent
public function onSwitchUser(SwitchUserEvent $event)
{
$this->auditLogger->log('USER_IMPERSONATION', [
'impersonated_user_id' => $event->getUser()->getId(),
'admin_user_id' => $event->getOriginalUser()->getId(),
]);
}
Multi-Tenant Support: Add tenant context to impersonation:
// Extend SwitchUserEvent
public function getTenant()
{
return $this->tenant;
}
Then pass the tenant in your resolver:
public function findUserByToken($token, $tenantId)
{
return User::where('tenant_id', $tenantId)
->where('api_token', $token)
->first();
}
**
How can I help you explore Laravel packages today?