Skip to content

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 routes

Auth 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 projects

Phầ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 router

Auth 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 missing

Fix 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 →

Powered by CodyMaster × VitePress