bburnichon/bbn-security-bundle
Install the Bundle
composer require bburnichon/bbn-security-bundle:@dev
Add to config/bundles.php (Symfony 4+) or AppKernel.php (Symfony 2/3):
BBn\SecurityBundle\BBnSecurityBundle::class => ['all' => true],
Configure security.yaml
Define a firewall with api_key authentication:
security:
firewalls:
api:
pattern: ^/api/
stateless: true
provider: api_user_provider
api_key:
parameter: X-API-KEY # Defaults to 'apikey' if omitted
Create a Custom User Provider
Extend UserInterface and implement loadUserByApiKey() (or override loadUserByUsername()):
use Symfony\Component\Security\Core\User\UserInterface;
class ApiUser implements UserInterface
{
public function loadUserByApiKey($apiKey)
{
// Fetch user by API key (e.g., from DB)
return new self($apiKey, '...', ['ROLE_USER']);
}
}
Register the Provider
In security.yaml:
providers:
api_user_provider:
entity: { class: App\Entity\ApiUser, property: apiKey }
Test the Flow
Send a request with the X-API-KEY header:
curl -H "X-API-KEY: your_api_key_here" http://your-app/api/endpoint
Request Handling
The bundle intercepts requests matching the firewall pattern and extracts the API key from the specified parameter (default: apikey query param or X-API-KEY header).
Example request:
curl -G --data-urlencode "apikey=your_key" http://your-app/api/data
User Loading
The api_key provider delegates to your custom UserProvider (e.g., loadUserByUsername() or loadUserByApiKey()). Ensure your provider returns a UserInterface object with roles.
Stateless Authentication
Since stateless: true is enforced, avoid sessions. Use tokens or JWTs for session-like behavior if needed.
Role-Based Access Attach roles to users in the provider:
return new ApiUser($apiKey, null, ['ROLE_API', 'ROLE_READ']);
Integration with Symfony Components
VoterInterface for granular permissions.security.interactive_login or security.authentication_success for post-auth logic.Provider Mismatch
UserProvider to handle API keys as usernames. If loadUserByUsername() fails, authentication silently fails. Test with:
$provider->loadUserByUsername('invalid_key'); // Should throw UsernameNotFoundException
Stateless Constraints
remember_me services.stateless: true in all related firewalls.Parameter Overrides
parameter in api_key config is case-sensitive. Defaults to:
apikeyX-API-KEYsecurity.yaml:
api_key:
parameter: Authorization # Custom header
API Key Exposure
firewall:
api:
pattern: ^/api
ip: 192.168.1.0/24
Enable Debugging
Add to config/packages/security.yaml:
security:
debug: true # Logs authentication attempts
Check User Loading
Override loadUserByUsername() temporarily to log API keys:
public function loadUserByUsername($apiKey)
{
error_log("API Key Attempt: $apiKey"); // Debug line
// ... rest of logic
}
Firewall Matching
Ensure the pattern in your firewall matches the request path exactly. Use regex if needed:
pattern: ^/api/v1/.*
Custom Authentication Logic
Extend the ApiKeyAuthenticator class (located in BBn\SecurityBundle\Security\Firewall\ApiKeyAuthenticator) to add pre-auth checks:
public function authenticate(Request $request)
{
$apiKey = $request->query->get('apikey');
if (empty($apiKey) || !$this->isKeyValid($apiKey)) {
throw new AuthenticationException('Invalid API key');
}
return $this->createToken($apiKey);
}
Dynamic Key Validation Integrate with a key validation service (e.g., Redis):
public function isKeyValid($apiKey)
{
return $this->redis->exists("api_key:$apiKey");
}
Rate Limiting
Combine with Symfony’s RateLimiter or a middleware to throttle API key usage:
use Symfony\Component\HttpFoundation\RequestStack;
public function __construct(RequestStack $requestStack, RateLimiterInterface $limiter)
{
$this->limiter = $limiter;
$this->requestStack = $requestStack;
}
public function authenticate(Request $request)
{
$apiKey = $request->headers->get('X-API-KEY');
if (!$this->limiter->isAllowed($apiKey, 100, 60)) {
throw new TooManyRequestsException();
}
// ... rest of logic
}
Logging
Log successful/failed attempts via Symfony’s LoggerInterface:
use Psr\Log\LoggerInterface;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function authenticate(Request $request)
{
$apiKey = $request->query->get('apikey');
$this->logger->info('API Key Auth Attempt', ['key' => $apiKey, 'ip' => $request->getClientIp()]);
// ... rest of logic
}
How can I help you explore Laravel packages today?