Xác thực (authentication) và phân quyền (authorization) là hai việc khác nhau. Auth xác định bạn là ai, authorization xác định bạn được làm gì.
Authentication — Bạn Là Ai?#
JWT — JSON Web Token#
JWT gồm 3 phần: header.payload.signature:
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjF9.abc123...plaintextimport jwt from 'jsonwebtoken';
// Tạo token
function generateTokens(userId: number, role: string) {
const accessToken = jwt.sign(
{ userId, role },
process.env.JWT_SECRET!,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId },
process.env.REFRESH_SECRET!,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
// Verify token (middleware)
function authenticate(req: Request, res: Response, next: NextFunction) {
const header = req.headers.authorization;
if (!header?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
try {
const token = header.split(' ')[1];
const payload = jwt.verify(token, process.env.JWT_SECRET!);
req.user = payload;
next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
}typescriptJWT Best Practices#
- Access token ngắn (15 phút) — giảm thiểu rủi ro nếu bị lộ
- Refresh token dài (7-30 ngày) — lưu trong httpOnly cookie
- Không lưu secret trong code — dùng env variable
- Secret mạnh — ít nhất 256-bit random
- Có rotation — đổi refresh token mỗi lần dùng
Refresh Token Flow#
app.post('/auth/refresh', async (req, res) => {
const token = req.cookies.refreshToken;
if (!token) return res.status(401).json({ error: 'No refresh token' });
try {
const payload = jwt.verify(token, process.env.REFRESH_SECRET!);
// Verify token còn trong DB (có thể revoke)
const stored = await db.refreshTokens.findUnique({ where: { token } });
if (!stored) return res.status(401).json({ error: 'Token revoked' });
// Xoá token cũ, tạo token mới (rotation)
await db.refreshTokens.delete({ where: { id: stored.id } });
const tokens = generateTokens(payload.userId, payload.role);
await db.refreshTokens.create({
data: { token: tokens.refreshToken, userId: payload.userId }
});
res.cookie('refreshToken', tokens.refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
res.json({ accessToken: tokens.accessToken });
} catch {
res.status(401).json({ error: 'Invalid refresh token' });
}
});typescriptOAuth2 — Ủy Quyền Cho Bên Thứ Ba#
OAuth2 cho phép user đăng nhập bằng Google, GitHub, Facebook mà không cần share password.
Flow Authorization Code#
User → App → Redirect → Google Login → Authorization Code → App Server
↓
App Server → Google → Access Token
↓
App Server dùng token gọi Google APIplaintext// 1. Redirect user đến Google
app.get('/auth/google', (req, res) => {
const params = new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID!,
redirect_uri: 'http://localhost:3000/auth/google/callback',
response_type: 'code',
scope: 'openid email profile',
});
res.redirect(`https://accounts.google.com/o/oauth2/auth?${params}`);
});
// 2. Callback — nhận code, đổi lấy token
app.get('/auth/google/callback', async (req, res) => {
const { code } = req.query;
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
body: new URLSearchParams({
code: code as string,
client_id: process.env.GOOGLE_CLIENT_ID!,
client_secret: process.env.GOOGLE_CLIENT_SECRET!,
redirect_uri: 'http://localhost:3000/auth/google/callback',
grant_type: 'authorization_code',
}),
});
const { access_token } = await tokenResponse.json();
const userResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { Authorization: `Bearer ${access_token}` },
});
const user = await userResponse.json();
// Tìm hoặc tạo user trong DB, tạo session
});typescriptAuthorization — Bạn Được Làm Gì?#
RBAC — Role-Based Access Control#
enum Role {
USER = 'user',
MODERATOR = 'moderator',
ADMIN = 'admin',
}
const permissions: Record<Role, string[]> = {
[Role.USER]: ['read:own', 'write:own'],
[Role.MODERATOR]: ['read:any', 'write:own', 'delete:own'],
[Role.ADMIN]: ['read:any', 'write:any', 'delete:any'],
};
function authorize(...required: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
const userRole = req.user.role as Role;
const userPerms = permissions[userRole];
const hasAll = required.every(p => userPerms.includes(p));
if (!hasAll) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// Usage
app.delete('/users/:id', authenticate, authorize('delete:any'), deleteUser);typescriptABAC — Attribute-Based Access Control#
function canAccessResource(user: User, resource: Resource): boolean {
// Admin access everything
if (user.role === 'admin') return true;
// User chỉ access resource của mình
if (resource.ownerId === user.id) return true;
// Moderator xoá bài vi phạm
if (user.role === 'moderator' && resource.type === 'post' && resource.flagged) {
return true;
}
return false;
}typescriptBảo Vệ Endpoint#
Rate Limiting#
import rateLimit from 'express-rate-limit';
// Global — 100 request/phút
app.use(rateLimit({
windowMs: 60 * 1000,
max: 100,
}));
// Auth endpoints — strict hơn
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: 'Too many login attempts',
});
app.use('/auth/login', authLimiter);typescriptInput Validation#
import { z } from 'zod';
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(8).max(100),
});
app.post('/auth/login', (req, res) => {
const result = loginSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'Validation failed',
details: result.error.flatten(),
});
}
// result.data — an toàn, đã validate
});typescriptCSRF Protection#
import csrf from 'csurf';
app.use(csrf({ cookie: true }));
// Gửi CSRF token trong form
app.get('/form', (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});typescriptSecurity Headers#
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Security-Policy: default-src 'self'
Referrer-Policy: strict-origin-when-cross-originbashPassword — Không Bao Giờ Lưu Plain Text#
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12;
async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
// Signup
app.post('/auth/signup', async (req, res) => {
const hashedPassword = await hashPassword(req.body.password);
await db.users.create({
data: {
email: req.body.email,
passwordHash: hashedPassword, // Không lưu password gốc
},
});
});typescriptLogout & Revoke#
app.post('/auth/logout', async (req, res) => {
// Xoá refresh token khỏi DB
await db.refreshTokens.deleteMany({
where: { userId: req.user.id },
});
res.clearCookie('refreshToken');
res.json({ message: 'Logged out' });
});
// Revoke tất cả sessions của user
app.post('/auth/revoke-all', authenticate, async (req, res) => {
await db.refreshTokens.deleteMany({
where: { userId: req.user.id },
});
res.json({ message: 'All sessions revoked' });
});typescriptChecklist#
- JWT với access token ngắn hạn + refresh token rotation
- Password hash bằng bcrypt (cost ≥ 12)
- Rate limiting trên auth endpoints
- Input validation (Zod, Joi)
- CSRF protection cho cookie-based auth
- Security headers (helmet)
- HTTPS mandatory
- CORS whitelist
- Audit log cho hành động nhạy cảm
- Dependency scan (
npm audit)
Kết Luận#
Security là layered defense. JWT cho stateless auth, refresh token cho UX, OAuth2 cho third-party login, RBAC/ABAC cho authorization. Không có “đủ an toàn” — chỉ có “đủ an toàn cho hôm nay”.