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:
- User logs in → server creates a JWT signed with a secret key.
- Server returns JWT to client.
- Client stores JWT (localStorage or HttpOnly cookie).
- On each request, client sends JWT in
Authorization: Bearer <token>header. - 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)