Customer portal MFA and recovery codes
Engineering reference for customer-portal multi-factor authentication in ethica-api: TOTP plus recovery codes (same conceptual model as ERP staff MFA, separate implementation and keys).
Model
- TOTP from an authenticator app (
CustomerMfaCredential, encrypted secret withCUSTOMER_MFA_ENCRYPTION_KEY). - Recovery codes: independent one-time codes, bcrypt-hashed at rest in
customer_mfa_credentials.backupCodesHashes(JSON array of hashes). Each successful use removes one hash. MFA stays enabled after using a code at login (this is not a full MFA reset).
Count: 10 codes per completed enrollment (staff ERP uses 8 in staff/mfa.service.ts; customers use the constant in customer-mfa.service.ts).
Codes are not interchangeable with staff codes (different issuer, encryption key, and database rows).
Enrollment and display
- After
POST /customer-auth/mfa/complete, the API returns plaintext codes once; the client must show them with copy, download as.txt, and explicit user acknowledgment before continuing (same UX pattern as ERPMfaEnrollmentPanel).
Login step-up
POST /customer-auth/login/mfaaccepts either a 6-digit TOTP or one recovery code.- Use a customer-specific request DTO so
codeallows backup code length (do not cap at staffLoginMfaDtomax length if it blocks hex codes). - The OTP UI must accept non-digit input for the recovery path (not digits-only).
Exhaustion (no codes left)
When the last hash is consumed:
- Email: Transactional message to the customer’s contact email (same resolution as other portal emails, e.g. portal credentials), warning that no recovery codes remain and pointing to Settings to regenerate. Requires
RESEND_API_KEY(and mail from-address) on the API. GET /customer-auth/me: Expose a non-secret field such asrecoveryCodesRemaining(or an exhausted flag) for dashboard/settings banners.
Settings and regeneration (planned / product-dependent)
- Settings should include change password and regenerate recovery codes.
- Regeneration: authenticated endpoint (e.g.
POST /customer-auth/mfa/recovery-codes/regenerate) requiring current password + TOTP, replaces all hashes with a new set, returns plaintext once, rate-limited, and ideally audit-logged.
Environment variables (names only)
| Variable | Purpose |
|---|---|
CUSTOMER_MFA_ENCRYPTION_KEY | Required in production. 32-byte value, base64-encoded; encrypts customer TOTP secrets. Do not reuse staff MFA_ENCRYPTION_KEY. |
CUSTOMER_MFA_ISSUER | Optional; issuer shown in authenticator apps. |
Other JWT/session variables align with shared security.config.ts and customer session handling.
Database
customer_mfa_credentials:customerPortalAccountId,organizationId,encryptedSecret,enabled,backupCodesHashes.
API routes (customer-auth)
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /customer-auth/login | No | Password login; may return mfaRequired + preAuthToken. |
| POST | /customer-auth/login/mfa | No | preAuthToken + TOTP or one recovery code → full JWT. |
| GET | /customer-auth/me | JWT | Identity, mfaEnrollmentRequired, recoveryCodesRemaining (or equivalent). |
| GET | /customer-auth/mfa/setup | JWT | Start TOTP enrollment (pending until verified). |
| POST | /customer-auth/mfa/complete | JWT | Verify TOTP; enable MFA; return plaintext recovery codes once. |
| PATCH | /customer-auth/password | JWT | Change password. |
| POST | /customer-auth/mfa/recovery-codes/regenerate | JWT | When implemented: password + TOTP → new codes; invalidates old. |
Related code (non-exhaustive)
ethica-api/src/customer-auth/customer-mfa.service.tsethica-api/src/customer-auth/customer-auth.service.tsethica-api/src/customer-auth/customer-auth.controller.tsethica-api/src/models/customer-mfa-credential.model.tsethica-api/src/mail/templates/(exhaustion email template, when present)
See also
- Authentication and authorization — staff vs customer overview.
- Operations — Customer portal recovery codes — end-user oriented wording.