0

JWT — JSON Web Token: self-contained, signed token for stateless auth

Intermediate5 min read·eng-19-007
interviewsecurity

Concept

JWT (JSON Web Token) — a compact, self-contained token format for securely transmitting information between parties. Used for stateless authentication.

Structure: Three base64url-encoded parts separated by dots: header.payload.signature

  • Header: Algorithm and token type. {"alg": "HS256", "typ": "JWT"}.
  • Payload: Claims (data). {"sub": "1", "name": "Alice", "exp": 1718000000}. NOT encrypted — base64-encoded (readable by anyone).
  • Signature: HMAC-SHA256(header + "." + payload, secret). Server verifies this.

How JWT authentication works:

  1. User logs in → server creates a JWT signed with a secret key.
  2. Server returns JWT to client.
  3. Client stores JWT (localStorage or HttpOnly cookie).
  4. On each request, client sends JWT in Authorization: Bearer <token> header.
  5. Server verifies the signature → if valid, trusts the payload (no DB lookup needed).

Stateless: Server doesn't store sessions. Any server can verify any JWT as long as it has the secret/public key. Scales horizontally with no shared session storage.

Standard claims:

  • sub: Subject (user ID).
  • iat: Issued at (timestamp).
  • exp: Expiration time.
  • iss: Issuer.
  • aud: Audience.

Problems with JWT:

  • Cannot revoke: If a token is compromised before expiry, you can't invalidate it (unless you maintain a blocklist — which makes it stateful again).
  • Payload is readable: Don't put sensitive data in JWT payload.
  • Algorithm confusion: "alg: none" attack. Always explicitly validate the algorithm.

Laravel: Sanctum can issue opaque tokens (DB-backed) or JWT-like tokens. For JWT specifically, use tymon/jwt-auth or lcobucci/jwt.

Code Example

php
<?php
// JWT STRUCTURE — decode and inspect (without verification)
$token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';

[$headerB64, $payloadB64, $signature] = explode('.', $token);

$header  = json_decode(base64_decode($headerB64), true);
// ["alg" => "HS256", "typ" => "JWT"]

$payload = json_decode(base64_decode($payloadB64), true);
// ["sub" => "1234567890", "name" => "Alice", "iat" => 1516239022]
// NOTE: Anyone can read this! Never put passwords or sensitive data here.

// CREATING a JWT (using firebase/php-jwt)
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

$payload = [
    'sub'  => $user->id,
    'name' => $user->name,
    'iat'  => time(),
    'exp'  => time() + (60 * 60 * 24), // expires in 24 hours
];

$jwt = JWT::encode($payload, config('jwt.secret'), 'HS256');
// Returns: "eyJhbGci..."

// VERIFYING a JWT
try {
    $decoded = JWT::decode($jwt, new Key(config('jwt.secret'), 'HS256'));
    $userId = $decoded->sub; // trust this — signature verified
} catch (\Firebase\JWT\ExpiredException $e) {
    return response()->json(['error' => 'Token expired'], 401);
} catch (\Exception $e) {
    return response()->json(['error' => 'Invalid token'], 401);
}

// LARAVEL SANCTUM — opaque tokens (safer, DB-backed)
// Better than JWT for most Laravel apps
$token = $user->createToken('api-access', ['read', 'write']);
return response()->json(['token' => $token->plainTextToken]);

// Authorization: Bearer <token> → Sanctum validates against personal_access_tokens table
// Can be revoked: $user->tokens()->delete();

// JWT for mobile/SPA — when stateless is required
// NEVER put JWT in localStorage if XSS is a concern
// Use HttpOnly cookie instead (XSS-resistant, but then need CSRF protection)