Files
goodgo-platform/AUTH_IMPLEMENTATION_CHECKLIST.md
Ho Ngoc Hai 25420720e7 fix(api,ci): remove type-only imports for DI and isolate CI ports from dev
- Remove `type` keyword from NestJS injectable class imports across all
  modules to fix runtime DI resolution (330+ handler/listener files)
- Offset CI docker-compose ports (5433/6380/8109/9002) to avoid
  conflicts with running dev containers
- Update .env.test, playwright.config.ts, and e2e workflow to use
  isolated CI ports with configurable overrides
- Fix prisma/seed.ts to use deterministic IDs for Prisma 7 upsert
  compatibility (phoneHash replaced phone as unique index)
- Add dedicated Docker bridge network for CI service containers

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
2026-04-13 01:40:14 +07:00

11 KiB

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)

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

const phone = '0900000001';
const normalized = `+84${phone.slice(1)}`; // '+84900000001'

Step 2: Derive HMAC Key

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

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

const passwordHash = await bcrypt.hash('AdminPassword123', 12);

Step 5: Create User

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

curl -X POST http://localhost:3000/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "0900000001",
    "password": "AdminPassword123"
  }'

Expected Response (Success)

{
  "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)

// 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!
  },
});

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

  • 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