Skip to content

Browser cookies and CSRF

Configure hardened first-party browser sessions with double-submit CSRF protection.

Cookie transport stores access and refresh credentials in httpOnly cookies. JavaScript cannot read those credentials, but the browser sends them automatically; unsafe methods therefore require CSRF protection.

transport: transports.cookie({
  secure: true,
  sameSite: 'lax',
  accessCookie: 'jwt_access',
  refreshCookie: 'jwt_refresh',
  csrfCookie: 'jwt_csrf',
  csrfHeader: 'x-csrf-token',
  accessPath: '/',
  refreshPath: '/auth/refresh',
})

SameSite is restricted to lax or strict. Keep secure: true outside local HTTP tests. The refresh cookie is scoped to the refresh route, reducing where the long-lived credential is sent. The package writes the CSRF cookie without httpOnly so browser code can echo it.

Send the CSRF proof

For POST, PUT, PATCH, and DELETE, read the jwt_csrf cookie and copy its exact value into x-csrf-token:

await fetch('/auth/refresh', {
  method: 'POST',
  credentials: 'include',
  headers: { 'x-csrf-token': readCookie('jwt_csrf') },
})

The guard compares the cookie and header with a timing-safe equality check. Missing or mismatched proof raises E_CSRF_TOKEN_MISMATCH. GET, HEAD, and OPTIONS do not require the header.

Deployment checklist

  • Terminate TLS before the application and leave secure: true.
  • Use one canonical first-party origin when possible.
  • Set domain only when trusted sibling subdomains must share authentication.
  • Do not widen the refresh cookie path beyond the refresh endpoint.
  • Keep state-changing routes on unsafe HTTP methods; never mutate state through GET.
  • If @adonisjs/shield also performs CSRF validation, align the exempt routes and header name so both layers enforce the same browser contract.

Logout clears all three cookies with the same path, domain, SameSite, and secure attributes used when writing them.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close