Installation:
composer require depthbomb/csrf-bundle
Ensure your project meets the requirements (PHP >= 8.1, Symfony 6.3.x).
Enable the Bundle:
Add Depthbomb\CsrfBundle\DepthbombCsrfBundle::class to the config/bundles.php array.
First Use Case:
Protect a controller or method with the #[CsrfProtected] attribute:
use Depthbomb\CsrfBundle\Attribute\CsrfProtected;
#[CsrfProtected('edit_user')]
public function edit(User $user): Response
{
// Your logic here
}
Generate a Token: In a controller or service:
$token = $this->tokenManager->getToken('edit_user');
Or in Twig:
<input type="hidden" name="csrf_token" value="{{ csrf_token('edit_user') }}">
Validate the Token:
The bundle automatically validates the token when the protected route is accessed. If invalid, a 428 Precondition Required error is thrown.
Token Generation:
$this->tokenManager->getToken('token_id') to generate tokens dynamically.csrf_token() function for static forms.return $this->json(['csrf_token' => $this->tokenManager->getToken('api_action')]);
Token Validation:
CsrfTokenManager or overriding the CsrfListener.Integration with Forms:
<form method="POST">
{{ csrf_token('form_action') }}
<!-- form fields -->
</form>
Route-Specific Protection:
#[CsrfProtected('token_id')].Token Storage:
CsrfTokenStorage service.Symfony UX Turbo/Stimulus: Use tokens in Turbo/Stimulus-driven forms to prevent CSRF on partial updates. Example:
{{ csrf_token('turbo_action')|raw }} {# Hidden in Turbo stream #}
APIs with JWT:
If using JWT, generate tokens server-side and include them in the Authorization header:
$token = $this->tokenManager->getToken('jwt_action');
return $this->json(['token' => $token]);
Client-side, send the token as:
Authorization: Bearer <csrf_token>
Custom Token Namespaces:
Use namespaced token IDs (e.g., user.123.edit) to avoid collisions in multi-tenant apps.
Token Mismatch:
#[CsrfProtected('token_id')]) does not match the token generated in the template/API (csrf_token('token_id')), validation fails with a 428 error.Session Dependence:
Double Submissions:
CsrfTokenManager to allow multiple uses:
# config/packages/depthbomb_csrf.yaml
depthbomb_csrf:
token_manager:
allow_multiple_uses: true
Caching Issues:
CsrfTokenStorage).Attribute Override:
#[CsrfProtected] at both the class and method level will use the method-level token ID.Token Not Found:
428 Errors:
Token Expiration:
lifetime in config:
depthbomb_csrf:
token_manager:
lifetime: 3600 {# 1 hour #}
Custom Token Storage:
Extend Depthbomb\CsrfBundle\Security\CsrfTokenStorageInterface to store tokens in a database or cache:
class DatabaseCsrfTokenStorage implements CsrfTokenStorageInterface
{
public function generateToken(string $id): string { /* ... */ }
public function isTokenValid(string $id, string $token): bool { /* ... */ }
}
Register it as a service:
services:
Depthbomb\CsrfBundle\Security\CsrfTokenStorageInterface: '@database_csrf_token_storage'
Override Validation Logic:
Extend the CsrfListener to customize validation (e.g., allow empty tokens for specific routes):
class CustomCsrfListener extends CsrfListener
{
protected function isCsrfTokenValid(Request $request, string $tokenId): bool
{
if ($request->getPathInfo() === '/public-route') {
return true;
}
return parent::isCsrfTokenValid($request, $tokenId);
}
}
Override the service in config/services.yaml:
Depthbomb\CsrfBundle\EventListener\CsrfListener: '@custom_csrf_listener'
Token Generation Hooks:
Subscribe to the csrf.token.generate event to modify tokens before generation:
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
#[AsEventListener(event: 'csrf.token.generate')]
public function onCsrfTokenGenerate(CsrfTokenEvent $event): void
{
if ($event->getTokenId() === 'admin_action') {
$event->setToken('admin_' . bin2hex(random_bytes(4)));
}
}
API Headers:
Modify the bundle to accept tokens in headers (e.g., X-CSRF-Token) instead of form fields:
// Extend CsrfListener and override getCsrfToken()
protected function getCsrfToken(Request $request): ?string
{
return $request->headers->get('X-CSRF-Token') ?? parent::getCsrfToken($request);
}
Default Configuration:
The bundle uses sensible defaults (e.g., 428 status code, session storage). Override in config/packages/depthbomb_csrf.yaml:
depthbomb_csrf:
http_status_code: 403 {# Change error code #}
token_manager:
storage: '@custom_token_storage'
Environment-Specific Tokens: Use parameter bags to define tokens per environment:
# config/packages/dev/depthbomb_csrf.yaml
depthbomb_csrf:
token_manager:
dev_tokens:
- 'dev_only_action'
Then check in your listener:
if (in_array($tokenId, $this->container->getParameter('dev_tokens'))) {
// Allow in dev
}
How can I help you explore Laravel packages today?