skip to content

For JavaScript requests to a Laravel app, how do the X-CSRF-TOKEN header, the XSRF-TOKEN cookie and the X-XSRF-TOKEN header differ?

level: middleimportance: should knowfreq 48%

answer

  1. plain token versus encrypted token
  2. meta tag filled from csrf_token()
  3. XSRF-TOKEN cookie is not HttpOnly
  4. Axios copies it into X-XSRF-TOKEN
  5. never put the cookie value in X-CSRF-TOKEN

basics

~10 s

X-CSRF-TOKEN carries the plain session token, usually read from a csrf_token() meta tag. XSRF-TOKEN is a cookie holding the token encrypted; clients like Axios echo it in X-XSRF-TOKEN, which Laravel decrypts before comparing.

solid answer

~40 s

`PreventRequestForgery` accepts the token from three places. The `_token` input or the `X-CSRF-TOKEN` header must carry the **plain** token, which you usually print into `<meta name="csrf-token" content="{{ csrf_token() }}">` and attach to requests yourself. The middleware also sets an `XSRF-TOKEN` cookie on responses; its value is **encrypted** by the cookie encrypter and it is not `HttpOnly`, so JavaScript can read it. Axios and Angular copy that cookie into an `X-XSRF-TOKEN` header on same-origin requests, and the middleware decrypts it, strips the cookie prefix and compares. The classic bug is copying the cookie into `X-CSRF-TOKEN`: the encrypted string never equals the plain token, so the request fails with 419. The header is only consulted when neither `_token` nor `X-CSRF-TOKEN` is present.

code

html · 1 line
html
<meta name="csrf-token" content="{{ csrf_token() }}">

go deeper

for a junior

Know that JavaScript writes need the CSRF token too, usually from a csrf-token meta tag sent as X-CSRF-TOKEN.

for a middle

Explain the plain versus encrypted carriers, the order in which the middleware reads them, and why Axios works with no setup.

for a senior

Debug 419s in JavaScript clients: mismatched header and value, stale meta tags after login, and precedence between headers.

for a principal

Decide how front-end teams obtain the token across pages, SPAs and mobile webviews, and when origin checks should replace it.

## Three carriers for one token Every Laravel session holds a single CSRF token under `_token`. `PreventRequestForgery` reads a candidate token from the request in this order: 1. the `_token` field in the request input; 2. the `X-CSRF-TOKEN` request header; 3. only if both are empty, the `X-XSRF-TOKEN` request header, which it **decrypts** first. It then compares the candidate with the session token using `hash_equals()`. The three carriers exist because forms, hand-written JavaScript and HTTP client libraries each find a different one convenient. | Carrier | Holds | Who sets it | Typical client | |---|---|---|---| | `_token` input | plain token | `@csrf` / `csrf_field()` | HTML forms | | `X-CSRF-TOKEN` header | plain token | your code, from `csrf_token()` | `fetch`, legacy jQuery setups | | `X-XSRF-TOKEN` header | encrypted token | the client library, copied from the `XSRF-TOKEN` cookie | Axios, Angular | ## The meta-tag pattern For a page that sends its own `fetch` calls, print the plain token once and attach it to every write: - render `<meta name="csrf-token" content="{{ csrf_token() }}">` in the layout; - read it in JavaScript and send it as `X-CSRF-TOKEN` on `POST`, `PUT`, `PATCH` and `DELETE` requests. The value is only valid for the current session. After a login regenerates the session, a page that kept the old meta tag sends a stale token and receives 419. ## The XSRF-TOKEN cookie On every response that passes through it, the middleware adds an `XSRF-TOKEN` cookie containing the session token. Three properties matter: - **It is encrypted.** The `EncryptCookies` middleware in the `web` group encrypts it like other cookies, so its value is a long base64 payload, not the token. - **It is not `HttpOnly`.** It is created with the HttpOnly flag off so that JavaScript on your origin can read it. - **It follows the session cookie settings** for lifetime, path, domain, `secure` and `same_site`. Axios and Angular are built around this convention: they read a cookie named `XSRF-TOKEN` and send its value in an `X-XSRF-TOKEN` header on same-origin requests. Laravel's middleware decrypts the header with the encrypter, strips the cookie-value prefix and compares the result, so those libraries work with no extra code. ## The mistake that produces 419 A common hand-rolled version reads the `XSRF-TOKEN` cookie and sends it as `X-CSRF-TOKEN`. Because `X-CSRF-TOKEN` is compared as-is, the encrypted cookie value never equals the plain session token, and the middleware throws `TokenMismatchException` (419). The fix is to pair each value with its header: - plain token from `csrf_token()` goes in `X-CSRF-TOKEN`; - encrypted cookie value goes in `X-XSRF-TOKEN`. A second trap: if a request carries both a stale `X-CSRF-TOKEN` and a valid `X-XSRF-TOKEN`, the stale plain header wins, because the encrypted header is consulted only when the other two are empty. ## Where Sec-Fetch-Site fits In Laravel 13 the middleware checks the `Sec-Fetch-Site` header before any token. A same-origin `fetch` over HTTPS from a modern browser therefore passes even with no token. The token headers still matter for plain-HTTP development, for clients that do not send the header, and for any deployment that relies on the token fallback. A single-page app on a different origin that authenticates with cookies is a separate topic handled by Sanctum's SPA flow. ## Debugging a 419 from JavaScript When a script-driven write returns 419, check in this order: 1. **Which header was sent?** Open the request in the browser's network panel and confirm whether `X-CSRF-TOKEN` or `X-XSRF-TOKEN` is present, and which value it carries. 2. **Is the value the right kind?** A long base64 string in `X-CSRF-TOKEN` is the encrypted cookie in the wrong header. 3. **Is it current?** Compare the meta tag's value after a login or logout; a page rendered before the session was regenerated holds a stale token. 4. **Did the session cookie travel?** Without the session cookie the server starts a new session whose token matches nothing.

  • Why is the XSRF-TOKEN cookie deliberately not HttpOnly when the session cookie is?
    Its whole purpose is to be read by JavaScript on your own origin and echoed in `X-XSRF-TOKEN`. Other origins cannot read it, so a forging page cannot copy it into a header. The session cookie, by contrast, only needs to travel with requests and is kept away from scripts.
  • What happens to the XSRF-TOKEN cookie when the app turns on preventRequestForgery(originOnly: true)?
    The middleware stops adding it: `shouldAddXsrfTokenCookie()` returns false in origin-only mode, because no token is checked any more. Clients that relied on echoing the cookie simply send nothing, and requests are judged on `Sec-Fetch-Site` alone.

saying these in an interview costs you the question

  • The XSRF-TOKEN cookie holds the same plain value as csrf_token().
  • Send the XSRF-TOKEN cookie value in the X-CSRF-TOKEN header.
  • The XSRF-TOKEN cookie is HttpOnly, so JavaScript cannot read it.
  • A valid X-XSRF-TOKEN overrides a stale X-CSRF-TOKEN header.
  • The browser attaches X-XSRF-TOKEN automatically without any library.