Chương 6: Authentication — Bảo Mật Từ Ngày Đầu

"Hà hỏi: 'Sao không để auth cuối sprint?' Đức đáp: 'Vì mỗi API đều cần biết AI LÀ AI.' Tuấn nhớ lại bug lần trước: JWT secret hardcoded ngay trong code. Lần này sẽ khác."
🎯 Mục tiêu
- Implement Supabase Auth (register/login/JWT)
- Viết auth middleware cho Express
- TDD approach: tests FIRST
- 🐛 Phát hiện Bug #3: JWT secret hardcoded
Phần 1: Supabase Auth Architecture (15 phút)
User → Register/Login → Supabase Auth → JWT Token
│
Express Middleware ← Verify Token ─────────┘
│
└→ req.user = { id, email, role }
→ Access protected routesAuth Flow
1. Client: POST /api/auth/register { email, password, name }
2. Server: supabase.auth.signUp() → creates user + JWT
3. Client stores JWT in localStorage
4. Client: GET /api/projects (Authorization: Bearer <JWT>)
5. Server: middleware verifies JWT → extracts user
6. Server: returns user's projectsPhần 2: TDD — Tests First (30 phút)
Lab: Write Auth Tests BEFORE Code
bash
antigravity "@cm-tdd Write auth tests for TeamFlow:
File: test/auth.test.ts
Test cases:
1. POST /api/auth/register — success (201, returns token)
2. POST /api/auth/register — duplicate email (409)
3. POST /api/auth/register — missing fields (400)
4. POST /api/auth/register — weak password (400, < 6 chars)
5. POST /api/auth/login — success (200, returns token)
6. POST /api/auth/login — wrong password (401)
7. POST /api/auth/login — non-existent email (401)
8. GET /api/auth/me — with valid token (200, user profile)
9. GET /api/auth/me — without token (401)
10. GET /api/auth/me — with expired token (401)
Use: Vitest + Supertest
Pattern: describe/it/expect"Run Tests (All Should FAIL — Red Phase)
bash
npm test -- test/auth.test.ts
# Expected: 10 tests FAIL ❌ — no implementation yet!Phần 3: Implementation (30 phút)
Auth Routes
typescript
// src/routes/auth.ts
import { Router } from 'express'
import { supabase } from '../lib/supabase'
import { z } from 'zod'
const router = Router()
const registerSchema = z.object({
email: z.string().email(),
password: z.string().min(6),
name: z.string().min(1).max(100)
})
router.post('/register', async (req, res) => {
const parsed = registerSchema.safeParse(req.body)
if (!parsed.success) {
return res.status(400).json({ error: parsed.error.issues[0].message })
}
const { email, password, name } = parsed.data
const { data, error } = await supabase.auth.signUp({
email,
password,
options: { data: { name } }
})
if (error) {
const status = error.message.includes('already') ? 409 : 400
return res.status(status).json({ error: error.message })
}
// Create profile
if (data.user) {
await supabase.from('profiles').insert({
id: data.user.id,
name,
role: 'dev'
})
}
res.status(201).json({
token: data.session?.access_token,
user: { id: data.user?.id, email, name }
})
})
router.post('/login', async (req, res) => {
const { email, password } = req.body
const { data, error } = await supabase.auth.signInWithPassword({
email, password
})
if (error) {
return res.status(401).json({ error: 'Invalid credentials' })
}
res.json({
token: data.session.access_token,
user: { id: data.user.id, email: data.user.email }
})
})
export default routerAuth Middleware
typescript
// src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express'
import { supabase } from '../lib/supabase'
// 🐛 BUG #3: Hardcoded fallback secret (intentional for student discovery)
const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-key-change-me'
export async function requireAuth(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' })
}
const token = authHeader.split(' ')[1]
const { data: { user }, error } = await supabase.auth.getUser(token)
if (error || !user) {
return res.status(401).json({ error: 'Invalid or expired token' })
}
;(req as any).user = user
next()
}Run Tests (Green Phase)
bash
npm test -- test/auth.test.ts
# Expected: 10 tests PASS ✅Phần 4: 🐛 Bug Hunt — Security (15 phút)
Hà (QA) phát hiện:
bash
antigravity "@cm-secret-shield Scan TeamFlow source code for hardcoded secrets"Output:
⚠️ SECURITY: Hardcoded secret found!
File: src/middleware/auth.ts:4
Line: const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-key-change-me'
Risk: HIGH — fallback secret in production = anyone can forge tokens
Fix: Remove fallback, require env variable, fail fast if missingFix Bug #3
typescript
// ✅ Fixed version
const JWT_SECRET = process.env.JWT_SECRET
if (!JWT_SECRET) {
throw new Error('JWT_SECRET environment variable is required')
}Quiz & Homework
Q: TDD approach cho auth giúp gì?
- A) Code nhanh hơn
- B) Biết chính xác auth hoạt động đúng TRƯỚC khi ship ✅
- C) AI viết test tốt hơn code
Homework:
- [ ] Complete auth implementation
- [ ] All 10 tests pass
- [ ] Fix Bug #3
- [ ] Add password strength validation
Chương tiếp: CRUD Mastery — Projects & Tasks →