# 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
