Root now contains only essential files: README.md, CLAUDE.md, CHANGELOG.md, CONTRIBUTING.md Reorganized into: docs/audits/ — all audit reports & checklists (71 files) docs/architecture/ — codebase overview, implementation plan docs/guides/ — auth guide, implementation checklist docs/load-testing/ — k6 load test guides & endpoints docs/security/ — payment & security reviews Also removed 5 untracked debug/investigation files and cleaned up playwright-report/ & test-results/ artifacts. Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
11 KiB
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_ROUNDSenv 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 asadmin@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 inemailHashphone→ stored encrypted + hash inphoneHashkycData→ 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 charsFIELD_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:
- Normalize phone
- Find user by phoneHash
- Check
isActive= true - Compare password (bcrypt)
- Check
totpEnabled - 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 offersSELLER- Can create listingsAGENT- Professional agent with verified profileADMIN- 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_SECRETor fallback toJWT_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)
phoneHashcomputed (HMAC-SHA256)emailHashcomputed (HMAC-SHA256)isActive: truetotpEnabled: 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
passwordHashis 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!
},
});
Recommended Enhancement
Use SEED_GENERATION_SCRIPT.ts to create users with full auth capability.
🔍 Troubleshooting
User Can't Login
- Verify
passwordHashis NOT null:SELECT id, passwordHash FROM "User" WHERE id = 'user-id'; - Check
isActive = true - Verify phone is normalized to
+84...format - Test phone normalization function directly
Phone Hash Mismatch
- Verify
FIELD_ENCRYPTION_KEYis same across deployments - Check hash computation:
HMAC-SHA256(lowercased_phone, hmacKey) - HKDF derivation must use exact string:
"goodgo-field-hash"
MFA Not Working
- Verify
MFA_BACKUP_CODE_SECRETis set - Check TOTP secret is encrypted properly
- Test clock skew (±30s tolerance)
Encryption/Decryption Issues
- Verify key is exactly 32 bytes (64 hex chars)
- Check IV length (12 bytes)
- Verify auth tag (16 bytes)
- 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