In a Laravel 13 Blade form, what does @csrf add, and when does a POST without a valid token fail with a 419 error?
answer
- a hidden field named _token
- csrf_token() reads the session token
- PreventRequestForgery in the web group
- Sec-Fetch-Site: same-origin skips the token
- TokenMismatchException becomes 419 Page Expired
basics
~20 s@csrf renders a hidden _token input holding the session's CSRF token. In Laravel 13, PreventRequestForgery lets same-origin browser requests through by Sec-Fetch-Site; otherwise a missing or stale token throws TokenMismatchException, rendered as 419 Page Expired.
solid answer
~50 s`@csrf` compiles to `csrf_field()`, a hidden `<input name="_token">` whose value is `csrf_token()`, the random token stored in the user's session. The `PreventRequestForgery` middleware, which is in the `web` group, lets a request through when it is a `GET`, `HEAD` or `OPTIONS` request, when the app is running unit tests, when the URI is excluded, when the browser's `Sec-Fetch-Site` header says `same-origin`, or when the submitted token matches the session's. Otherwise it throws `TokenMismatchException` ("CSRF token mismatch."), which the exception handler turns into HTTP 419, shown as "Page Expired". In Laravel 13 a same-origin form over HTTPS in a modern browser therefore passes even without `@csrf`. The 419 appears for requests without that header: plain HTTP, older browsers, cross-site posts, server-to-server calls. The token itself goes stale when the session expires or is regenerated, so an old tab gets 419 too. Keep `@csrf` in every form.
code
html · 8 lines<form method="POST" action="/profile">
@csrf
<input type="text" name="display_name">
<button type="submit">Save</button>
</form>
<!-- @csrf renders: -->
<input type="hidden" name="_token" value="{{ csrf_token() }}" autocomplete="off">go deeper
Know that @csrf adds a hidden _token field, that the check runs on POST, PUT, PATCH and DELETE in the web group, and that a mismatch shows 419 Page Expired.
Explain the middleware's order of checks, where the token comes from, and why an expired or regenerated session produces 419 on an old tab.
Diagnose unexpected 419s across cookies, domains, caches and HTTP vs HTTPS, and explain why Sec-Fetch-Site does not make @csrf optional.
Set the team's stance on forgery defences: token fallback versus origin-only mode, and how to verify it given tests skip the middleware.
## What @csrf renders Laravel generates a random 40-character **CSRF token** for each session and keeps it in the session under `_token`. The Blade directive `@csrf` compiles to `csrf_field()`, which outputs: ```html <input type="hidden" name="_token" value="...the session token..." autocomplete="off"> ``` `csrf_token()` returns the same value, so you can also place it in a meta tag or pass it to JavaScript. The token is **per session**, not per form: every form on every page carries the same value until the session's token is regenerated, which happens when the session ID is regenerated, typically at login. ## The checks PreventRequestForgery runs `Illuminate\Foundation\Http\Middleware\PreventRequestForgery` sits in the default `web` middleware group, after the session has started. In Laravel 13 it lets a request through if **any** of these holds, in this order: 1. the method is `GET`, `HEAD` or `OPTIONS` (reads are not checked); 2. the application is running unit tests; 3. the URI matches an excluded pattern; 4. the `Sec-Fetch-Site` request header is `same-origin`, or `same-site` when same-site requests are allowed; 5. the token from the `_token` input, the `X-CSRF-TOKEN` header or the decrypted `X-XSRF-TOKEN` header matches the session token, compared with `hash_equals()`. If none holds, it throws `Illuminate\Session\TokenMismatchException` with the message "CSRF token mismatch." ## How a mismatch becomes 419 The framework's exception handler maps `TokenMismatchException` to an `HttpException` with status **419**, a non-standard code Laravel uses for "the page you submitted from has expired". The built-in error page reads "Page Expired". The exception is on the handler's internal do-not-report list, so it does not fill your logs. ## What Laravel 13 changed Before Laravel 13 the middleware went by `VerifyCsrfToken` and `ValidateCsrfToken`, and a missing token always meant 419. Laravel 13 renamed it to `PreventRequestForgery`, keeping the old names as deprecated subclasses, and added the `Sec-Fetch-Site` check. Browsers send that header over HTTPS and mark a form posted from your own pages as `same-origin`, so such a request passes before the token is even read. | Request | Laravel 13 result without a valid token | |---|---| | Same-origin form over HTTPS, modern browser | passes on `Sec-Fetch-Site` | | Same form over plain HTTP or from a browser without the header | 419 | | Form auto-submitted from another site | 419 | | Server-to-server POST (no browser headers) | 419 | ## Why forms still need @csrf - **HTTP and older clients.** Local development over plain HTTP, and browsers that do not send `Sec-Fetch-Site`, rely on the token. - **Defence in depth.** The token fallback is what protects you when the header is absent. - **Origin-only mode is a separate decision.** Only an explicit configuration turns the token check off. ## The usual causes of an unexpected 419 - **The session expired.** After `SESSION_LIFETIME` minutes idle, a new session with a new token starts, so an old tab submits a token the server no longer has. - **The session was regenerated.** Logging in on another tab regenerates the session and its token. - **The session cookie never arrived.** A wrong session domain, a `secure` cookie on plain HTTP, or a page served from a different host than the form's action. - **A cached page.** A full-page cache served one user's HTML, with its token, to others. - **A missing `@csrf`** on a form that is submitted over HTTP or from an older browser. Feature tests will not catch a missing `@csrf`: the middleware skips its checks while unit tests run.
- Why does a feature test that posts a form without @csrf pass even though the real form would fail over HTTP?`PreventRequestForgery` returns early when the app is running unit tests, so no token is checked during `$this->post(...)`. The test proves the controller works, not that the form carries a token. A browser test, or a unit test that exercises the middleware directly, is needed to catch a missing `@csrf`.
- Why does a user who left a form open overnight get 419 Page Expired when submitting it?The session expired after `SESSION_LIFETIME` idle minutes, so the next request starts a new session with a new token. The `_token` in the old page no longer matches. If the browser sends `Sec-Fetch-Site: same-origin` over HTTPS the CSRF check passes, but the user is no longer logged in, so on an `auth`-protected route they are redirected to log in instead.
saying these in an interview costs you the question
- @csrf generates a new token for each form it renders.
- 419 means the user lacks permission for the action.
- In Laravel 13 every POST without _token is rejected with 419.
- Feature tests fail when a form forgets @csrf.
- A GET route that changes data is protected by the CSRF middleware.