0

JWT authentication — how it works vs sessions, stateless trade-offs

Advanced5 min read·lv-16-009
securityinterviewcompare

Concept

Comparing JWT to session-based authentication requires understanding what each stores and where. The choice between them is a fundamental architecture decision, not a style preference.

Session-based authentication (Laravel's default):

  • After login, server creates a session record (DB or Redis) with a session ID.
  • Browser receives the session ID in a Set-Cookie header.
  • On every request, the cookie is sent automatically; server looks up session data.
  • Revocation: Immediate — delete the session record.
  • State: Server-side. All state lives in the session store.
  • Scaling: Requires shared session storage (Redis/DB) when multiple servers.

JWT authentication:

  • After login, server generates and signs a JWT encoding the user's claims.
  • Client stores JWT (localStorage, sessionStorage, or httpOnly cookie).
  • Every request sends JWT in Authorization: Bearer {token} header.
  • Server verifies the signature — no DB lookup needed for validation.
  • Revocation: Hard — can't invalidate without a server-side blocklist (defeating statelessness).
  • State: Client-side token, no server storage for verification.
  • Scaling: Trivially horizontal — any server with the secret can verify.

When JWT makes sense:

  • Microservices where one service issues tokens consumed by another.
  • Mobile apps where cookies are less convenient.
  • True stateless architectures (serverless).

When sessions are better:

  • Traditional web apps — cookies are automatic and secure.
  • When you need instant revocation (logout everywhere).
  • When SPA + API: use Sanctum cookie auth (session-backed) for simplicity.

Code Example

php
// Session auth — what happens on Auth::attempt()
// 1. Password verified against DB
// 2. Session record created: ['_token' => csrf, 'login_web_xxx' => user_id, ...]
// 3. Response: Set-Cookie: laravel_session=<session_id>; HttpOnly; SameSite=Lax

// Subsequent requests:
// Cookie: laravel_session=<session_id>
// Laravel decrypts cookie → looks up session data → finds user_id → loads User

// JWT flow — stateless
// 1. POST /api/login → server returns: {"access_token": "eyJhb..."}
// 2. Client stores token in localStorage or memory
// 3. Subsequent requests: Authorization: Bearer eyJhb...
// 4. Server verifies signature, reads payload.sub (user_id), loads User
// No DB session lookup — only signature verification

// Revocation comparison:
// Sessions: DB::table('sessions')->where('user_id', $userId)->delete(); — immediate
// JWT:      Add token jti (ID) to a blocklist — adds DB lookup, defeats statelessness

// Hybrid: Sanctum + cookie (recommended for SPAs)
// - Cookie-based, session-backed auth via SPA middleware
// - Combines the convenience of stateless token headers with proper revocation
// - Route::middleware('auth:sanctum') handles both SPA cookies and API tokens

// Practical recommendation:
// API (mobile/third-party): Sanctum tokens or Passport
// SPA same-domain: Sanctum SPA auth (session + cookies)
// Microservices: JWT or OAuth2 with Passport
// JWT only when truly stateless scaling is needed and instant revocation is not