# GoodGo Platform - Authentication Implementation Checklist **Date:** April 12, 2026 **Status:** ✅ Complete Analysis --- ## 📋 Authentication System Components ### ✅ 1. Password Hashing - **Algorithm:** bcrypt - **Salt Rounds:** 12 (configurable via `BCRYPT_ROUNDS` env var) - **Min Password:** 8 characters - **Location:** `apps/api/src/modules/auth/domain/value-objects/hashed-password.vo.ts` - **Method Used:** `HashedPassword.fromPlain(password)` → async bcrypt.hash() - **Comparison:** `passwordHash.compare(plainPassword)` → bcrypt.compare() ### ✅ 2. Phone Validation & Normalization - **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}$/` - **Accepted Formats:** - `0900000001` (local) - `84900000001` (country code, no +) - `+84900000001` (international) - **Normalized Format:** Always `+84XXX...` (country code prefix) - **Carriers:** Mobile only (no landlines) - 32-39: Viettel/VinaPhone/MobiFone - 52, 56, 58, 59: Viettel - 70, 76-79: Newer carriers - 81-89: VinaPhone - 90-99: MobiFone ### ✅ 3. Email Validation & Normalization - **File:** `apps/api/src/modules/auth/domain/value-objects/email.vo.ts` - **Regex:** `/^[^\s@]+@[^\s@]+\.[^\s@]+$/` (basic validation) - **Normalization:** lowercase + trimmed - **Example:** `ADMIN@GOODGO.VN` → stored as `admin@goodgo.vn` ### ✅ 4. PII Encryption & Hashing - **File:** `apps/api/src/modules/shared/infrastructure/field-encryption.ts` - **Encryption Algorithm:** AES-256-GCM - **Key Size:** 32 bytes (64 hex characters) - **IV:** 12 bytes (random) - **Auth Tag:** 16 bytes - **Storage Format:** `enc:v{version}:{iv}:{authTag}:{ciphertext}` (hex) - **Encrypted Fields:** - `email` → stored encrypted + hash in `emailHash` - `phone` → stored encrypted + hash in `phoneHash` - `kycData` → stored encrypted (no separate hash) - **Hash Function:** HMAC-SHA256 (derived from encryption key via HKDF-SHA256) - **Hash Normalization:** lowercase + trimmed - **Env Vars:** - `FIELD_ENCRYPTION_KEY` (required) - hex string, 64 chars - `FIELD_ENCRYPTION_KEY_VERSION` (optional, default: 1) - Fallback: `KYC_ENCRYPTION_KEY` / `KYC_ENCRYPTION_KEY_VERSION` ### ✅ 5. Login Flow - **File:** `apps/api/src/modules/auth/infrastructure/strategies/local.strategy.ts` - **Username Field:** `phone` (Vietnamese format) - **Password Field:** `password` (plaintext) - **User Lookup:** By `phoneHash` (unique index) - **Steps:** 1. Normalize phone 2. Find user by phoneHash 3. Check `isActive` = true 4. Compare password (bcrypt) 5. Check `totpEnabled` 6. Issue JWT tokens or MFA challenge - **MFA Response (if enabled):** `challengeId` + 5-minute TTL - **No MFA Response:** `accessToken` + `refreshToken` + expiry ### ✅ 6. User Roles - **Enum:** `UserRole` (Prisma) - **Values:** - `BUYER` (default) - Can search, inquire, make offers - `SELLER` - Can create listings - `AGENT` - Professional agent with verified profile - `ADMIN` - Full platform access - **Default Role:** `BUYER` - **Admin Role:** Created explicitly with `role: 'ADMIN'` ### ✅ 7. MFA (Multi-Factor Authentication) - **TOTP:** - Generator: otplib (RFC 6238) - Period: 30 seconds - Digits: 6 - Clock Skew: ±30 seconds - **Backup Codes:** - Count: 10 - Length: 8 characters each - Charset: A-Z (no O, I), 2-9 (no 0, 1) - Hashing: HMAC-SHA256 (not bcrypt) - Secret Key: `MFA_BACKUP_CODE_SECRET` or fallback to `JWT_SECRET` - **TOTP Secret Storage:** Encrypted with AES-256-GCM ### ✅ 8. User Model Fields (Required for Login) ```typescript User { id: string // CUID phone: string // Normalized: +84XXX... phoneHash: string // HMAC-SHA256 (unique index) email?: string // Lowercase, trimmed (encrypted) emailHash?: string // HMAC-SHA256 (unique index) passwordHash?: string // bcrypt hash (nullable for OAuth) fullName: string role: UserRole // BUYER | SELLER | AGENT | ADMIN isActive: boolean // true = can login kycStatus: KYCStatus // NONE | PENDING | VERIFIED | REJECTED totpEnabled: boolean // MFA enabled totpSecret?: string // Encrypted totpBackupCodes: string[] // HMAC-SHA256 hashed codes createdAt: DateTime updatedAt: DateTime } ``` --- ## 🔐 Creating Login-Capable Seed Users ### Requirements Checklist - [ ] Password ≥ 8 characters - [ ] Phone matches Vietnamese regex - [ ] Phone normalized to `+84...` format - [ ] Email matches basic regex `^[^\s@]+@[^\s@]+\.[^\s@]+$` - [ ] Email lowercased - [ ] Password hashed with bcrypt (≥12 rounds) - [ ] `phoneHash` computed (HMAC-SHA256) - [ ] `emailHash` computed (HMAC-SHA256) - [ ] `isActive: true` - [ ] `totpEnabled: false` (for seed users) - [ ] `totpBackupCodes: []` ### Implementation Steps **Step 1: Normalize Phone** ```typescript const phone = '0900000001'; const normalized = `+84${phone.slice(1)}`; // '+84900000001' ``` **Step 2: Derive HMAC Key** ```typescript const encryptionKey = process.env['FIELD_ENCRYPTION_KEY']; // hex string const hmacKey = crypto.hkdfSync( 'sha256', Buffer.from(encryptionKey, 'hex'), Buffer.alloc(0), Buffer.from('goodgo-field-hash', 'utf8'), 32, ); ``` **Step 3: Compute Hashes** ```typescript const phoneHash = crypto .createHmac('sha256', hmacKey) .update(normalized.toLowerCase()) .digest('hex'); const emailHash = crypto .createHmac('sha256', hmacKey) .update(email.toLowerCase()) .digest('hex'); ``` **Step 4: Hash Password** ```typescript const passwordHash = await bcrypt.hash('AdminPassword123', 12); ``` **Step 5: Create 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: [], }, }); ``` --- ## 🧪 Testing Login ### Prerequisites - User exists in database - `passwordHash` is set (not null) - `isActive: true` - No MFA enabled (or have MFA code ready) ### Test Request ```bash curl -X POST http://localhost:3000/auth/login \ -H "Content-Type: application/json" \ -d '{ "phone": "0900000001", "password": "AdminPassword123" }' ``` ### Expected Response (Success) ```json { "requiresMfa": false, "tokens": { "accessToken": "eyJhbGc...", "refreshToken": "eyJhbGc...", "expiresIn": 3600 } } ``` ### Error Cases - **Invalid phone format:** "Số điện thoại không hợp lệ" - **User not found:** "Số điện thoại hoặc mật khẩu không đúng" - **User inactive:** "Tài khoản đã bị vô hiệu hóa" - **Wrong password:** "Số điện thoại hoặc mật khẩu không đúng" - **MFA required:** `{ "requiresMfa": true, "challengeId": "..." }` --- ## 📁 Key Files Reference | File | Purpose | Key Functions | |------|---------|---------------| | `hashed-password.vo.ts` | Password hashing | `fromPlain()`, `compare()` | | `phone.vo.ts` | Phone validation | `create()` | | `email.vo.ts` | Email validation | `create()` | | `vietnam-phone.validator.ts` | Phone regex/normalize | `isValidVietnamPhone()`, `normalizeVietnamPhone()` | | `field-encryption.ts` | PII encryption/hashing | `encryptField()`, `decryptField()`, `computeHash()` | | `local.strategy.ts` | Login flow | `validate()` | | `mfa.service.ts` | TOTP/backup codes | `generateSetup()`, `verifyTotp()`, `generateBackupCodes()` | | `user.entity.ts` | User domain model | `createNew()` | | `prisma-user.repository.ts` | User persistence | `findByPhone()`, `save()` | | `encrypt-pii-fields.ts` | Backfill encryption | Batch encryption migration | | `schema.prisma` | Database schema | User model, enums | | `seed.ts` | Seed data | Current seeds (no passwords) | --- ## 🚀 Deployment Checklist ### Environment Variables Required - [ ] `BCRYPT_ROUNDS` (optional, default: 12) - [ ] `FIELD_ENCRYPTION_KEY` (required for PII, hex string 64 chars) - [ ] `FIELD_ENCRYPTION_KEY_VERSION` (optional, default: 1) - [ ] `MFA_BACKUP_CODE_SECRET` (optional, fallback to JWT_SECRET) - [ ] `JWT_SECRET` (required for tokens) ### Database Setup - [ ] Run migrations (including `add_mfa_totp_support`) - [ ] Seed users with passwords (use provided script) - [ ] Test login functionality - [ ] Verify PII encryption working ### Testing - [ ] Test login with various phone formats (0900..., 84900..., +84900...) - [ ] Test invalid phone numbers (rejected) - [ ] Test password validation (min 8 chars) - [ ] Test email validation - [ ] Test MFA setup and verification - [ ] Test backup code generation/usage - [ ] Verify hashes computed correctly --- ## 📝 Current Seed Data Status ### Existing Seed (prisma/seed.ts) **Status:** ❌ **NOT login-capable** (no passwords) ```typescript // Current seed - users created without passwords const admin = await prisma.user.upsert({ where: { id: 'seed-user-admin' }, create: { id: 'seed-user-admin', phone: '0900000001', // NOT normalized/hashed email: 'admin@goodgo.vn', fullName: 'Admin GoodGo', role: UserRole.ADMIN, // passwordHash: null ← Cannot login! }, }); ``` ### Recommended Enhancement Use `SEED_GENERATION_SCRIPT.ts` to create users with full auth capability. --- ## 🔍 Troubleshooting ### User Can't Login 1. Verify `passwordHash` is NOT null: `SELECT id, passwordHash FROM "User" WHERE id = 'user-id';` 2. Check `isActive = true` 3. Verify phone is normalized to `+84...` format 4. Test phone normalization function directly ### Phone Hash Mismatch 1. Verify `FIELD_ENCRYPTION_KEY` is same across deployments 2. Check hash computation: `HMAC-SHA256(lowercased_phone, hmacKey)` 3. HKDF derivation must use exact string: `"goodgo-field-hash"` ### MFA Not Working 1. Verify `MFA_BACKUP_CODE_SECRET` is set 2. Check TOTP secret is encrypted properly 3. Test clock skew (±30s tolerance) ### Encryption/Decryption Issues 1. Verify key is exactly 32 bytes (64 hex chars) 2. Check IV length (12 bytes) 3. Verify auth tag (16 bytes) 4. Ensure `enc:` prefix detection working --- ## 📚 Additional Resources ### External Documentation - **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 ### Related Files - Registration flow: `register-user.handler.ts` - Token generation: `token.service.ts` - JWT strategy: `jwt.strategy.ts` - Refresh token: `refresh-token.handler.ts` --- **Last Updated:** April 12, 2026 **Platform:** GoodGo Real Estate Platform **Status:** ✅ Production-Ready