JSON Web Token (JWT) — Từ Cấu Trúc Đến Implementation
JWT deep dive: cấu trúc token, signing algorithms, refresh token rotation, blacklist, và security best practices.
JWT (JSON Web Token) là chuẩn token compact dùng cho authentication. Self-contained — mọi thông tin đều trong token, không cần session store.
Cấu Trúc JWT#
JWT gồm 3 phần, cách nhau bởi dấu chấm:
header.payload.signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cplaintextHeader#
{
"alg": "HS256", // Thuật toán sign
"typ": "JWT" // Type
}jsonPayload (Claims)#
{
"sub": "user_123", // Subject — user ID
"name": "Alice",
"role": "admin",
"iat": 1516239022, // Issued at
"exp": 1516242622, // Expiration
"iss": "myapp.com", // Issuer
"aud": "api.myapp.com" // Audience
}jsonSignature#
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)plaintextSigning Algorithms#
HS256 — Symmetric (HMAC + SHA256)#
import jwt from 'jsonwebtoken';
const secret = process.env.JWT_SECRET!; // Shared secret
const token = jwt.sign(
{ userId: 123, role: 'admin' },
secret,
{ expiresIn: '15m' }
);
const decoded = jwt.verify(token, secret);typescriptƯu: Nhanh, đơn giản. Nhược: Secret phải share giữa các service — nếu leak, tất cả đều ảnh hưởng.
RS256 — Asymmetric (RSA + SHA256)#
import jwt from 'jsonwebtoken';
import fs from 'fs';
const privateKey = fs.readFileSync('private.pem');
const publicKey = fs.readFileSync('public.pem');
// Sign với private key (chỉ auth server làm)
const token = jwt.sign(
{ userId: 123 },
privateKey,
{ algorithm: 'RS256', expiresIn: '15m' }
);
// Verify với public key (bất kỳ service nào)
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });typescriptƯu: Public key ai cũng có, private key chỉ auth server biết. Nhược: Chậm hơn HMAC, key pair management.
ES256 — Elliptic Curve (ECDSA)#
# Tạo key pair
openssl ecparam -genkey -name prime256v1 -noout -out private.pem
openssl ec -in private.pem -pubout -out public.pembashƯu: Chữ ký nhỏ hơn RSA, nhanh hơn, bảo mật tương đương.
Refresh Token Rotation#
interface TokenPair {
accessToken: string;
refreshToken: string;
refreshTokenId: string;
}
async function generateTokens(userId: number): Promise<TokenPair> {
const refreshTokenId = crypto.randomUUID();
const accessToken = jwt.sign(
{ userId, type: 'access' },
process.env.JWT_SECRET!,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId, tokenId: refreshTokenId, type: 'refresh' },
process.env.REFRESH_SECRET!,
{ expiresIn: '7d' }
);
// Lưu refresh token trong DB — có thể revoke
await db.refreshTokens.create({
data: {
id: refreshTokenId,
userId,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
},
});
return { accessToken, refreshToken, refreshTokenId };
}
async function refreshAccessToken(refreshToken: string): Promise<TokenPair> {
try {
const decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET!) as any;
// Kiểm tra token còn trong DB không (có thể đã revoke)
const stored = await db.refreshTokens.findUnique({
where: { id: decoded.tokenId },
});
if (!stored) throw new Error('Token revoked');
// Rotation — xoá token cũ, tạo token mới
await db.refreshTokens.delete({ where: { id: decoded.tokenId } });
return generateTokens(decoded.userId);
} catch {
throw new UnauthorizedError('Invalid refresh token');
}
}typescriptJWT Blacklist#
Khi user logout — access token vẫn còn hạn. Dùng blacklist (Redis):
// Logout — thêm token vào blacklist
async function logout(tokenId: string, expiresAt: number) {
const ttl = expiresAt - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await redis.set(`blacklist:${tokenId}`, 'true', 'EX', ttl);
}
}
// Middleware kiểm tra blacklist
async function authenticate(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization?.split('Bearer ')[1];
if (!token) return res.status(401).json({ error: 'Missing token' });
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any;
// Kiểm tra blacklist
const blacklisted = await redis.get(`blacklist:${decoded.jti}`);
if (blacklisted) throw new Error('Token revoked');
req.user = decoded;
next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
}typescriptJWT trong Cookie#
// Set cookie
res.cookie('access_token', token, {
httpOnly: true, // Chống XSS
secure: true, // Chỉ HTTPS
sameSite: 'strict', // Chống CSRF
maxAge: 15 * 60 * 1000, // 15 phút
path: '/',
});
// Middleware đọc từ cookie
function authenticate(req: Request, res: Response, next: NextFunction) {
const token = req.cookies.access_token;
if (!token) return res.status(401).json({ error: 'Not authenticated' });
// verify...
}typescriptJWT với JWKS (JSON Web Key Set)#
Dùng cho OAuth2/OIDC — public keys được serve qua endpoint:
// JWKS endpoint
app.get('/.well-known/jwks.json', async (req, res) => {
const publicKey = await getPublicKey();
res.json({
keys: [{
kty: 'RSA',
use: 'sig',
alg: 'RS256',
kid: 'key-1',
n: publicKey.n,
e: publicKey.e,
}],
});
});
// Client verify với JWKS
import { createRemoteJWKSet, jwtVerify } from 'jose';
const JWKS = createRemoteJWKSet(new URL('https://auth.example.com/.well-known/jwks.json'));
async function verifyToken(token: string) {
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://auth.example.com',
audience: 'api.example.com',
});
return payload;
}typescriptJWT Payload Size#
JWT không nên chứa quá nhiều dữ liệu — mỗi request đều gửi token:
// ❌ Tệ — quá nhiều claim
const token = jwt.sign({
userId: 123,
name: 'Alice',
email: 'alice@example.com',
avatar: 'https://...',
role: 'admin',
permissions: ['read', 'write', 'delete'],
department: 'engineering',
lastLogin: '2024-01-01',
preferences: { theme: 'dark', locale: 'vi' },
}, secret);
// ✅ Tốt — chỉ lưu identifier
const token = jwt.sign({
sub: 'user_123',
role: 'admin',
}, secret, { expiresIn: '15m' });typescriptSecurity Checklist#
- Access token ngắn (15 phút)
- Refresh token rotation
- JWT trong httpOnly cookie (cho web) hoặc Authorization header (cho mobile)
-
audclaim — kiểm tra audience -
issclaim — kiểm tra issuer - Blacklist cho logout
- Key rotation (RS256/ES256) — đổi key định kỳ
- Không lưu secret trong code
- Không decode token mà không verify
Kết Luận#
JWT là chuẩn authentication phổ biến vì:
- Stateless — không cần session store
- Portable — mobile, web, service-to-service
- Self-contained — user info trong token
Nhưng nhớ: JWT không thể revoke trước hạn (trừ khi dùng blacklist). Access token ngắn + refresh token rotation là giải pháp.