# GoodGo Platform - Checklist Triển Khai Authentication **Ngày:** 12 tháng 4, 2026 **Trạng thái:** ✅ Phân Tích Hoàn Tất --- ## 📋 Các Thành Phần Của Hệ Thống Authentication ### ✅ 1. Hashing Mật Khẩu - **Thuật toán:** bcrypt - **Salt Rounds:** 12 (có thể cấu hình qua biến env `BCRYPT_ROUNDS`) - **Mật khẩu tối thiểu:** 8 ký tự - **Vị trí:** `apps/api/src/modules/auth/domain/value-objects/hashed-password.vo.ts` - **Method dùng:** `HashedPassword.fromPlain(password)` → async bcrypt.hash() - **So sánh:** `passwordHash.compare(plainPassword)` → bcrypt.compare() ### ✅ 2. Validate & Chuẩn Hóa Số Điện Thoại - **File:** `apps/api/src/modules/shared/utils/vietnam-phone.validator.ts` - **Regex:** `/^(?:\+84|84|0)(3[2-9]|5[2689]|7[06-9]|8[1-9]|9[0-9])\d{7}$/` - **Định dạng được chấp nhận:** - `0900000001` (local) - `84900000001` (mã quốc gia, không +) - `+84900000001` (quốc tế) - **Định dạng chuẩn hóa:** Luôn là `+84XXX...` (prefix mã quốc gia) - **Nhà mạng:** Chỉ di động (không có cố định) - 32-39: Viettel/VinaPhone/MobiFone - 52, 56, 58, 59: Viettel - 70, 76-79: Nhà mạng mới - 81-89: VinaPhone - 90-99: MobiFone ### ✅ 3. Validate & Chuẩn Hóa Email - **File:** `apps/api/src/modules/auth/domain/value-objects/email.vo.ts` - **Regex:** `/^[^\s@]+@[^\s@]+\.[^\s@]+$/` (validate cơ bản) - **Chuẩn hóa:** lowercase + trim - **Ví dụ:** `ADMIN@GOODGO.VN` → lưu thành `admin@goodgo.vn` ### ✅ 4. Mã Hóa & Hashing PII - **File:** `apps/api/src/modules/shared/infrastructure/field-encryption.ts` - **Thuật toán mã hóa:** AES-256-GCM - **Kích thước key:** 32 byte (64 ký tự hex) - **IV:** 12 byte (ngẫu nhiên) - **Auth Tag:** 16 byte - **Định dạng lưu trữ:** `enc:v{version}:{iv}:{authTag}:{ciphertext}` (hex) - **Field được mã hóa:** - `email` → lưu mã hóa + hash trong `emailHash` - `phone` → lưu mã hóa + hash trong `phoneHash` - `kycData` → lưu mã hóa (không có hash riêng) - **Hàm hash:** HMAC-SHA256 (dẫn xuất từ encryption key qua HKDF-SHA256) - **Chuẩn hóa hash:** lowercase + trim - **Biến env:** - `FIELD_ENCRYPTION_KEY` (bắt buộc) - chuỗi hex, 64 ký tự - `FIELD_ENCRYPTION_KEY_VERSION` (tùy chọn, mặc định: 1) - Fallback: `KYC_ENCRYPTION_KEY` / `KYC_ENCRYPTION_KEY_VERSION` ### ✅ 5. Luồng Đăng Nhập - **File:** `apps/api/src/modules/auth/infrastructure/strategies/local.strategy.ts` - **Trường username:** `phone` (định dạng Việt Nam) - **Trường password:** `password` (plaintext) - **Lookup user:** Theo `phoneHash` (unique index) - **Các bước:** 1. Chuẩn hóa số điện thoại 2. Tìm user theo phoneHash 3. Kiểm tra `isActive` = true 4. So sánh password (bcrypt) 5. Kiểm tra `totpEnabled` 6. Cấp JWT token hoặc MFA challenge - **Phản hồi MFA (nếu bật):** `challengeId` + TTL 5 phút - **Phản hồi không có MFA:** `accessToken` + `refreshToken` + thời hạn ### ✅ 6. Vai Trò User - **Enum:** `UserRole` (Prisma) - **Giá trị:** - `BUYER` (mặc định) - Có thể tìm kiếm, hỏi, đặt giá - `SELLER` - Có thể tạo tin đăng - `AGENT` - Agent chuyên nghiệp với hồ sơ đã verify - `ADMIN` - Truy cập đầy đủ platform - **Vai trò mặc định:** `BUYER` - **Vai trò Admin:** Tạo rõ ràng với `role: 'ADMIN'` ### ✅ 7. MFA (Multi-Factor Authentication) - **TOTP:** - Generator: otplib (RFC 6238) - Period: 30 giây - Digit: 6 - Clock Skew: ±30 giây - **Backup Code:** - Số lượng: 10 - Độ dài: 8 ký tự mỗi mã - Charset: A-Z (không có O, I), 2-9 (không có 0, 1) - Hashing: HMAC-SHA256 (không phải bcrypt) - Secret Key: `MFA_BACKUP_CODE_SECRET` hoặc fallback về `JWT_SECRET` - **Lưu trữ TOTP Secret:** Mã hóa với AES-256-GCM ### ✅ 8. Field User Model (Bắt Buộc Cho Đăng Nhập) ```typescript User { id: string // CUID phone: string // Chuẩn hóa: +84XXX... phoneHash: string // HMAC-SHA256 (unique index) email?: string // Lowercase, trim (mã hóa) emailHash?: string // HMAC-SHA256 (unique index) passwordHash?: string // hash bcrypt (nullable cho OAuth) fullName: string role: UserRole // BUYER | SELLER | AGENT | ADMIN isActive: boolean // true = có thể đăng nhập kycStatus: KYCStatus // NONE | PENDING | VERIFIED | REJECTED totpEnabled: boolean // MFA đã bật totpSecret?: string // Mã hóa totpBackupCodes: string[] // Mã đã hash HMAC-SHA256 createdAt: DateTime updatedAt: DateTime } ``` --- ## 🔐 Tạo Seed User Có Khả Năng Đăng Nhập ### Checklist Yêu Cầu - [ ] Mật khẩu ≥ 8 ký tự - [ ] Số điện thoại khớp regex Việt Nam - [ ] Số điện thoại chuẩn hóa thành định dạng `+84...` - [ ] Email khớp regex cơ bản `^[^\s@]+@[^\s@]+\.[^\s@]+$` - [ ] Email lowercased - [ ] Mật khẩu hash với bcrypt (≥12 rounds) - [ ] `phoneHash` được tính (HMAC-SHA256) - [ ] `emailHash` được tính (HMAC-SHA256) - [ ] `isActive: true` - [ ] `totpEnabled: false` (cho seed user) - [ ] `totpBackupCodes: []` ### Các Bước Triển Khai **Bước 1: Chuẩn Hóa Số Điện Thoại** ```typescript const phone = '0900000001'; const normalized = `+84${phone.slice(1)}`; // '+84900000001' ``` **Bước 2: Dẫn Xuất HMAC Key** ```typescript const encryptionKey = process.env['FIELD_ENCRYPTION_KEY']; // chuỗi hex const hmacKey = crypto.hkdfSync( 'sha256', Buffer.from(encryptionKey, 'hex'), Buffer.alloc(0), Buffer.from('goodgo-field-hash', 'utf8'), 32, ); ``` **Bước 3: Tính Hash** ```typescript const phoneHash = crypto .createHmac('sha256', hmacKey) .update(normalized.toLowerCase()) .digest('hex'); const emailHash = crypto .createHmac('sha256', hmacKey) .update(email.toLowerCase()) .digest('hex'); ``` **Bước 4: Hash Mật Khẩu** ```typescript const passwordHash = await bcrypt.hash('AdminPassword123', 12); ``` **Bước 5: Tạo User** ```typescript await prisma.user.create({ data: { id: 'admin-seed-001', phone: normalized, // +84900000001 phoneHash, email, emailHash, passwordHash, fullName: 'Admin GoodGo', role: 'ADMIN', kycStatus: 'VERIFIED', isActive: true, totpEnabled: false, totpBackupCodes: [], }, }); ``` --- ## 🧪 Test Đăng Nhập ### Yêu Cầu Trước - User tồn tại trong database - `passwordHash` được set (không null) - `isActive: true` - Không bật MFA (hoặc có sẵn mã MFA) ### Request Test ```bash curl -X POST http://localhost:3000/auth/login \ -H "Content-Type: application/json" \ -d '{ "phone": "0900000001", "password": "AdminPassword123" }' ``` ### Phản Hồi Mong Đợi (Thành Công) ```json { "requiresMfa": false, "tokens": { "accessToken": "eyJhbGc...", "refreshToken": "eyJhbGc...", "expiresIn": 3600 } } ``` ### Các Trường Hợp Lỗi - **Định dạng số điện thoại không hợp lệ:** "Số điện thoại không hợp lệ" - **Không tìm thấy user:** "Số điện thoại hoặc mật khẩu không đúng" - **User không active:** "Tài khoản đã bị vô hiệu hóa" - **Sai mật khẩu:** "Số điện thoại hoặc mật khẩu không đúng" - **Yêu cầu MFA:** `{ "requiresMfa": true, "challengeId": "..." }` --- ## 📁 Tham Khảo Các File Chính | File | Mục đích | Hàm chính | |------|---------|---------------| | `hashed-password.vo.ts` | Hash mật khẩu | `fromPlain()`, `compare()` | | `phone.vo.ts` | Validate số điện thoại | `create()` | | `email.vo.ts` | Validate email | `create()` | | `vietnam-phone.validator.ts` | Regex/chuẩn hóa số điện thoại | `isValidVietnamPhone()`, `normalizeVietnamPhone()` | | `field-encryption.ts` | Mã hóa/hash PII | `encryptField()`, `decryptField()`, `computeHash()` | | `local.strategy.ts` | Luồng đăng nhập | `validate()` | | `mfa.service.ts` | TOTP/backup code | `generateSetup()`, `verifyTotp()`, `generateBackupCodes()` | | `user.entity.ts` | Domain model User | `createNew()` | | `prisma-user.repository.ts` | Persist user | `findByPhone()`, `save()` | | `encrypt-pii-fields.ts` | Backfill mã hóa | Migration mã hóa hàng loạt | | `schema.prisma` | Schema database | Model User, enum | | `seed.ts` | Dữ liệu seed | Seed hiện tại (không có mật khẩu) | --- ## 🚀 Checklist Deployment ### Biến Môi Trường Bắt Buộc - [ ] `BCRYPT_ROUNDS` (tùy chọn, mặc định: 12) - [ ] `FIELD_ENCRYPTION_KEY` (bắt buộc cho PII, chuỗi hex 64 ký tự) - [ ] `FIELD_ENCRYPTION_KEY_VERSION` (tùy chọn, mặc định: 1) - [ ] `MFA_BACKUP_CODE_SECRET` (tùy chọn, fallback về JWT_SECRET) - [ ] `JWT_SECRET` (bắt buộc cho token) ### Cài Đặt Database - [ ] Chạy migration (bao gồm `add_mfa_totp_support`) - [ ] Seed user với mật khẩu (dùng script đã cung cấp) - [ ] Test chức năng đăng nhập - [ ] Verify mã hóa PII hoạt động ### Testing - [ ] Test đăng nhập với nhiều định dạng số điện thoại (0900..., 84900..., +84900...) - [ ] Test số điện thoại không hợp lệ (bị từ chối) - [ ] Test validate mật khẩu (tối thiểu 8 ký tự) - [ ] Test validate email - [ ] Test setup và verify MFA - [ ] Test sinh/dùng backup code - [ ] Verify hash được tính đúng --- ## 📝 Trạng Thái Dữ Liệu Seed Hiện Tại ### Seed Hiện Có (prisma/seed.ts) **Trạng thái:** ❌ **KHÔNG có khả năng đăng nhập** (không có mật khẩu) ```typescript // Seed hiện tại - user được tạo không có mật khẩu const admin = await prisma.user.upsert({ where: { id: 'seed-user-admin' }, create: { id: 'seed-user-admin', phone: '0900000001', // CHƯA chuẩn hóa/hash email: 'admin@goodgo.vn', fullName: 'Admin GoodGo', role: UserRole.ADMIN, // passwordHash: null ← Không thể đăng nhập! }, }); ``` ### Cải Tiến Khuyến Nghị Dùng `SEED_GENERATION_SCRIPT.ts` để tạo user với khả năng auth đầy đủ. --- ## 🔍 Troubleshooting ### User Không Đăng Nhập Được 1. Verify `passwordHash` KHÔNG null: `SELECT id, passwordHash FROM "User" WHERE id = 'user-id';` 2. Kiểm tra `isActive = true` 3. Verify số điện thoại chuẩn hóa thành định dạng `+84...` 4. Test trực tiếp hàm chuẩn hóa số điện thoại ### Phone Hash Không Khớp 1. Verify `FIELD_ENCRYPTION_KEY` giống nhau giữa các deployment 2. Kiểm tra cách tính hash: `HMAC-SHA256(lowercased_phone, hmacKey)` 3. Dẫn xuất HKDF phải dùng đúng chuỗi: `"goodgo-field-hash"` ### MFA Không Hoạt Động 1. Verify `MFA_BACKUP_CODE_SECRET` đã được set 2. Kiểm tra TOTP secret được mã hóa đúng cách 3. Test clock skew (dung sai ±30s) ### Vấn Đề Mã Hóa/Giải Mã 1. Verify key đúng 32 byte (64 ký tự hex) 2. Kiểm tra độ dài IV (12 byte) 3. Verify auth tag (16 byte) 4. Đảm bảo phát hiện prefix `enc:` hoạt động --- ## 📚 Tài Nguyên Bổ Sung ### Tài Liệu Bên Ngoài - **bcrypt:** https://github.com/kelektiv/node.bcrypt.js - **otplib:** https://github.com/yeojz/otplib - **Prisma:** https://www.prisma.io/docs - **NestJS:** https://docs.nestjs.com ### File Liên Quan - Luồng đăng ký: `register-user.handler.ts` - Sinh token: `token.service.ts` - JWT strategy: `jwt.strategy.ts` - Refresh token: `refresh-token.handler.ts` --- **Cập nhật lần cuối:** 12 tháng 4, 2026 **Platform:** GoodGo Real Estate Platform **Trạng thái:** ✅ Sẵn sàng Production