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

Security Csrf Laravel Package

symfony/security-csrf

The Symfony Security CSRF component provides a CsrfTokenManager class for generating and validating cross-site request forgery tokens, helping protect your PHP applications from forged form submissions and malicious requests.

View on GitHub
Deep Wiki
Context7

Getting Started

Laravel already has session-based CSRF via VerifyCsrfToken, so install this only when you need something Laravel's helper does not give you: scoped token IDs, pluggable token storage (Redis/DB/cookie), rotatable per-action tokens, or framework-agnostic code shared with a Symfony app. Day one is a 15-minute integration.

composer require symfony/security-csrf:^7.4
# PHP 8.1 only (Laravel 10): ^6.4 · PHP 8.4 (Symfony 8): ^8.0

Look at these four contracts first — everything else is a concrete implementation:

Class Role
CsrfTokenManagerInterface getToken, refreshToken, removeToken, isTokenValid
TokenStorageInterface getToken (throws TokenNotFoundException), setToken, removeToken, hasToken
TokenGeneratorInterface DefaultTokenGenerator, UriSafeTokenGenerator
CsrfToken Immutable id + value value object

Step 1 — adapt Laravel's session to TokenStorageInterface (Symfony's NativeSessionTokenStorage needs $_SESSION and will not see Laravel's store):

namespace App\Security;

use Illuminate\Contracts\Session\Session;
use Symfony\Component\Security\Csrf\Exception\TokenNotFoundException;
use Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface;

class LaravelSessionTokenStorage implements TokenStorageInterface
{
    private const KEY = '_sf_csrf';

    public function __construct(private Session $session) {}

    public function getToken(string $tokenId): string
    {
        $value = $this->session->get(self::KEY.'.'.$tokenId);

        if (!\is_string($value)) {
            // MUST throw this exact exception: CsrfTokenManager catches it to mint a new token
            throw new TokenNotFoundException('No CSRF token stored for "'.$tokenId.'".');
        }

        return $value;
    }

    public function setToken(string $tokenId, #[\SensitiveParameter] string $token): void
    {
        $this->session->put(self::KEY.'.'.$tokenId, $token);
    }

    public function removeToken(string $tokenId): ?string
    {
        return $this->session->pull(self::KEY.'.'.$tokenId);
    }

    public function hasToken(string $tokenId): bool
    {
        return \is_string($this->session->get(self::KEY.'.'.$tokenId));
    }
}

Step 2 — bind it (use scoped, not singleton, see Gotchas):

// AppServiceProvider::register()
$this->app->scoped(TokenStorageInterface::class, fn ($app) => new LaravelSessionTokenStorage($app['session.store']));

$this->app->scoped(CsrfTokenManagerInterface::class, fn ($app) => new CsrfTokenManager(
    new \Symfony\Component\Security\Csrf\TokenGenerator\UriSafeTokenGenerator(),
    $app->make(TokenStorageInterface::class),
    'app_', // namespace prefix so IDs never collide with anything else
));

Step 3 — first use case: a token scoped to one form. In the controller that renders the form:

$token = app(CsrfTokenManagerInterface::class)->getToken('contact_form')->getValue();

return view('contact', ['csrf' => $token]);
<form method="POST" action="/contact">
    @csrf
    <input type="hidden" name="_csrf_token" value="{{ $csrf }}">
</form>

Then verify it in the receiving controller/middleware. That's the whole loop: one ID on the way in, the same ID on the way out. Everything below builds on it.

Implementation Patterns

Pattern 1 — A parameterised verification middleware

The most common day-to-day shape: one middleware, token ID passed as a route middleware parameter.

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Session\TokenMismatchException; // Laravel renders this as HTTP 419
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;

class VerifyScopedCsrf
{
    public function __construct(private CsrfTokenManagerInterface $csrf) {}

    public function handle(Request $request, Closure $next, string $tokenId = 'default')
    {
        if (\in_array($request->method(), ['GET', 'HEAD', 'OPTIONS'], true)) {
            return $next($request);
        }

        $value = $request->input('_csrf_token') ?? $request->header('X-CSRF-Token');
        $token = new CsrfToken($tokenId, $value !== null ? (string) $value : null);

        if (!$this->csrFIsValid($token)) {
            throw new TokenMismatchException('CSRF token mismatch.');
        }

        return $next($request);
    }

    private function csrFIsValid(CsrfToken $token): bool
    {
        try {
            return $this->csrf->isTokenValid($token);
        } catch (\Throwable) {
            return false;
        }
    }
}
Route::post('/contact', ContactController::class)
    ->middleware('csrf.scoped:contact_form');
// bootstrap/app.php (Laravel 11+) or Kernel::$routeMiddleware
->withMiddleware(fn ($m) => $m->alias(['csrf.scoped' => \App\Http\Middleware\VerifyScopedCsrf::class]))

Pattern 2 — Per-form scoping (the real reason to reach for this package)

Laravel's _token is global to the session: one token defends every form. symfony/security-csrf lets you mint distinct tokens per action, so a token leaked from a low-value form cannot authorise a destructive one.

// Render
$id = "user.{$user->id}.delete";
return view('account.edit', ['deleteToken' => $manager->getToken($id)->getValue()]);

// Handle (one-off action)
if (!$manager->isTokenValid(new CsrfToken($id, $request->input('_csrf_token')))) {
    throw new TokenMismatchException();
}
$manager->removeToken($id);   // or refreshToken($id) to keep the form reusable

Rules of thumb: IDs must be stable between render and submit, and they must match [a-zA-Z0-9_] (see Gotchas). Good ID schemes: contact_form, user.42.delete, checkout.payment. Bad: a raw request value, a timestamp, or a route name you might rename.

Pattern 3 — SPA / JSON API with session cookies

Cookie-authenticated SPAs still need CSRF. Expose the token as JSON rather than a hidden field:

Route::get('/csrf-token', fn () => response()
    ->json(['token' => app(CsrfTokenManagerInterface::class)->getToken('spa')->getValue()])
    ->header('Cache-Control', 'no-store'))
    ->middleware('web');

The client fetches it once per page load, stores it in memory, and sends X-CSRF-Token on every mutation. The middleware from Pattern 1 reads the header, so no code change. Requirements: credentials: 'include' on the fetch, a session cookie that can actually be sent (SameSite=Lax for same-origin; SameSite=None; Secure for cross-origin), and Cache-Control: no-store so a proxy never serves a stale token.

Pattern 4 — Redis/cache storage instead of the session

Because storage is just an interface, you can move tokens out of the session entirely — useful for stateless-ish APIs where the session is thin, or when you want a TTL:

class CacheTokenStorage implements TokenStorageInterface
{
    public function __construct(private \Illuminate\Contracts\Cache\Repository $cache) {}

    public function getToken(string $tokenId): string
    {
        return $this->cache->get($this->key($tokenId))
            ?? throw new TokenNotFoundException();
    }

    public function setToken(string $tokenId, string $token): void
    {
        $this->cache->put($this->key($tokenId), $token, now()->addHours(2));
    }

    public function removeToken(string $tokenId): ?string { return $this->cache->pull($this->key($tokenId)); }
    public function hasToken(string $tokenId): bool { return $this->cache->has($this->key($tokenId)); }

    private function key(string $id): string { return 'csrf:'.sha1($id); }
}

Note the sha1 — it sidesteps the character restrictions and keeps cache keys short.

Pattern 5 — Testing

Generate the token against the same manager the app uses:

public function test_delete_requires_scoped_token(): void
{
    $user = User::factory()->create();

    $this->actingAs($user)
        ->delete("/users/{$user->id}", ['_csrf_token' => 'bogus'])
        ->assertStatus(419);

    $token = app(CsrfTokenManagerInterface::class)->getToken("user.{$user->id}.delete")->getValue();

    $this->actingAs($user)
        ->delete("/users/{$user->id}", ['_csrf_token' => $token])
        ->assertRedirect('/users');
}

$this->withoutMiddleware(VerifyScopedCsrf::class) works too, but test at least one happy path with a real token — that is what catches ID mismatches.

Gotchas and Tips

  • Tokens are not single-use. isTokenValid() is a pure hash_equals() comparison against storage; it does not invalidate. If replay matters, call refreshToken($id) (or removeToken($id)) immediately after successful validation.
  • Token IDs have a character whitelist. Symfony's storage implementations reject anything outside [a-zA-Z0-9_] with an InvalidArgumentException. If you derive IDs from user input, preg_replace('/[^a-zA-Z0-9_]/', '_', $raw) first — otherwise you get a 500 in production that never shows up in tests with clean fixtures.
  • A missing token must throw TokenNotFoundException, not return null. CsrfTokenManager catches that exception to mint a first token; returning null from getToken() makes getToken() blow up on a fresh session. This is the #1 custom-adapter bug.
  • Do not bind the manager as an unscoped singleton under Octane. A singleton constructed with $app['session.store'] will hand a worker's previous request's session to the next one. Bind with scoped(), or resolve the session lazily inside the storage adapter ($this->app->make('session.store') per call). Same class of bug applies to NativeSessionTokenStorage, which is outright unsafe under Octane because $_SESSION persists in the worker.
  • Your middleware runs in tests; Laravel's doesn't. VerifyCsrfToken short-circuits via runningUnitTests(). Custom middleware has no such escape hatch, so existing feature tests that POST without a token will start returning 419 after you wire it up. Grep your test suite before rolling out.
  • The namespace prefix is silently prepended. new CsrfTokenManager($gen, $store, 'app_') stores key app_contact_form but reports the ID as contact_form. Don't add the prefix yourself in both places.
  • SessionTokenStorage vs NativeSessionTokenStorage vs LaravelSessionTokenStorage. Only the first two ship with the package; the first wants a Symfony\Component\HttpFoundation\Session\SessionInterface (Laravel doesn't provide one), the second wants session_start(). In Laravel, always write the ~20-line adapter shown above.
  • Token rotation on privilege change. Call removeToken() on login, logout, password change, and impersonation start/stop. Otherwise a pre-auth token keeps working post-auth if the session ID isn't regenerated.
  • Multi-tab surprise. Same token ID ⇒ same token value. Refreshing a token in tab A invalidates the half-submitted form in tab B. Scope per-form IDs (Pattern 2) or accept the trade-off explicitly.
  • SameOriginCsrfTokenManager (7.2+) is tempting but not drop-in. It skips server-side storage entirely and validates the request's Origin/Referer/Sec-Fetch-Site headers against the host (see changelog fixes #59269 "trust Referer at the same level as Origin", #62077 "add Sec-Fetch-Site", #59146 "use the session only if started"). It relies on a Symfony HttpKernel response listener to issue its cookie — read SameOriginCsrfProtectionListener in symfony/security-http and port that listener to a Laravel middleware before using it. Great for header-carrying API clients; not a one-liner.
  • Version alignment. Laravel already pulls several symfony/* packages; pin to the same major to avoid a duplicate tree: ^6.4 for PHP 8.1 / Laravel 10, ^7.4 for PHP 8.2–8.3 / Laravel 11–12, ^8.0 only on PHP 8.4. Run composer why-not symfony/security-csrf ^7.4 when resolution fails — it names the conflicting component.
  • Extension points worth knowing. Implement TokenGeneratorInterface for a custom format (e.g. UUIDv7); decorate CsrfTokenManagerInterface to add logging or metrics around validation failures (a spike in 419s is a useful signal); implement TokenStorageInterface for Redis, a database table, or an HMAC-signed stateless token where isTokenValid() recomputes the signature instead of reading storage. The last one is the cleanest path to true statelessness and about 40 lines of code.
  • Never log token values. Mark setToken()'s parameter #[\SensitiveParameter] (as above) so exception traces don't leak it, and keep tokens out of your response-caching layer entirely.
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.
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
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core