Web Security
Beginner
2 min read
JWT Security: Pitfalls and Best Practices
Example
<?php
// Secure JWT handling with firebase/php-jwt
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
$privateKey = file_get_contents('/run/secrets/jwt_private_key'); // PEM RSA key
$publicKey = file_get_contents('/run/secrets/jwt_public_key');
// --- ISSUE an access token (RS256 asymmetric signing) ---
function issueAccessToken(int $userId, string $email): string
{
global $privateKey;
$now = time();
$payload = [
'iss' => 'https://api.example.com', // issuer
'aud' => 'https://app.example.com', // audience
'sub' => (string) $userId, // subject
'iat' => $now, // issued at
'exp' => $now + 900, // 15-minute expiry
'email' => $email,
// Do NOT include: passwords, PII beyond what's needed, secrets
];
return JWT::encode($payload, $privateKey, 'RS256');
}
// --- VERIFY a token (explicit algorithm — never accept 'alg' from header) ---
function verifyAccessToken(string $token): object
{
global $publicKey;
$decoded = JWT::decode($token, new Key($publicKey, 'RS256'));
// Validate additional claims
if ($decoded->iss !== 'https://api.example.com') {
throw new \RuntimeException('Invalid issuer');
}
if ($decoded->aud !== 'https://app.example.com') {
throw new \RuntimeException('Invalid audience');
}
return $decoded;
}
// --- Refresh token stored in HttpOnly cookie, not in JWT payload ---
// Set-Cookie: refresh_token=<opaque_token>; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh
Related Resources
Web Security Reference
Complete tag & property list
Web Security How-To Guides
Step-by-step practical guides
Web Security Exercises
Practice what you've learned
More in Web Security