MFA & OTP v2.0.0
Source: src/services/otp/
Hệ thống con OTP cung cấp vòng đời mật khẩu một lần thống nhất, dùng Redis làm backend, được mọi luồng MFA trong identity service sử dụng. Một abstract base class duy nhất xử lý việc sinh code, hashing, rate limit, lockout và quản lý session. Các provider cụ thể giao mã qua email hoặc SMS, còn các business service triển khai logic xác minh đặc thù theo nghiệp vụ.
Kiến trúc
Cấu trúc File
services/otp/
├── base/
│ ├── base-otp.service.ts ← BaseOTPBasedMFAService (abstract)
│ ├── provider/
│ │ ├── email-otp.service.ts ← EmailOtpService (delivery only)
│ │ ├── phone-otp.service.ts ← PhoneOtpService (delivery only)
│ │ └── index.ts
│ └── index.ts
├── verify-email.service.ts ← VerifyEmailService
├── verify-phone.service.ts ← VerifyPhoneService
├── forgot-password.service.ts ← ForgotPasswordService
├── link-account.service.ts ← LinkAccountService
├── types.ts ← TOtpProvider, TOtpSessionData, TOtpVerifyResult
└── index.tsBase Service — BaseOTPBasedMFAService
Source: src/services/otp/base/base-otp.service.ts
Lớp generic abstract sở hữu toàn bộ vòng đời OTP. Người gọi cung cấp namespace, đối tượng config, session data và các cờ rate-limit.
Public API
| Method | Signature | Mô tả |
|---|---|---|
send | send(opts) => { sessionToken, expiresAt } | Kiểm tra lock, cooldown, daily limit; sinh code và hash bằng Bun.password; lưu trên Redis; gọi _deliver() |
verifyByCode | verifyByCode(opts) => TVerifyResult | Resolve session, kiểm tra lock, validate hash của code, tăng counter attempts khi thất bại, lock nếu vượt max, gọi _onVerified() khi thành công |
resolveSession | resolveSession(opts) => TSessionData | Lấy session data từ Redis theo token |
Abstract Members
Subclass phải triển khai:
| Member | Mục đích |
|---|---|
_redis | Instance Redis helper được inject |
_deliver({ target, code, config }) | Gửi code qua email hoặc SMS |
_onVerified(sessionData) | Handler sau khi xác minh |
Redis Key Schema
Tất cả key được prefix bằng tham số namespace truyền lúc gọi send.
| Key Pattern | Value | TTL |
|---|---|---|
{ns}:otp:{identifier} | Code hash, attempts, expiresAt | codeExpirySeconds |
{ns}:lock:{identifier} | Cờ lockout | lockoutSeconds |
{ns}:session:{token} | Session data JSON | codeExpirySeconds |
{ns}:cooldown:{identifier} | Cờ cooldown gửi lại | resendCooldownSeconds |
{ns}:daily:{identifier} | Bộ đếm yêu cầu trong ngày | Đến nửa đêm |
Config Interface
interface IBaseOTPBasedMFAConfig {
codeLength: number;
codeExpirySeconds: number;
maxAttempts: number;
lockoutSeconds: number;
resendCooldownSeconds?: number;
maxResendsPerDay?: number;
}Providers
EmailOtpService
Source: src/services/otp/base/provider/email-otp.service.ts
_deliver({ target, code, config })— gửi quaIMailServicevới template có thể cấu hình (appName,templateName,templateDataKey)._onVerified(sessionData)— trảTOtpVerifyResult.- Config kế thừa base với:
appName,templateName,templateDataKey.
PhoneOtpService
Source: src/services/otp/base/provider/phone-otp.service.ts
_deliver({ target, code })— gửi SMS quaIOTPSender._onVerified(sessionData)— trảTOtpVerifyResult.
Nguyên tắc thiết kế
Provider không bao giờ parse identifier. Chúng nhận trực tiếp email hoặc số điện thoại từ sessionData.target.
Business Services
VerifyEmailService
Source: src/services/otp/verify-email.service.ts
Sign-in và đăng ký bằng email không cần xác thực trước.
sendOtp({ email })gọiemailOtp.send()với namespacemfa:email:verify_email, bật cooldown, bật giới hạn theo ngày.verifyOtp({ sessionToken, otp })triển khai logic ba nhánh tùy theo trạng thái email trong database.
VerifyPhoneService
Source: src/services/otp/verify-phone.service.ts
Sign-in và đăng ký bằng SĐT không cần xác thực trước, kèm liên kết customer.
sendOtp({ phone })gọiphoneOtp.send()với namespacemfa:phone:verify_phone, không cooldown, không giới hạn theo ngày.- Nhánh đăng ký chạy
_matchAndLinkCustomers(): tìm các bản ghiCustomerchưa link khớp số điện thoại và gắn chúng với user mới và organizer của họ.
ForgotPasswordService
Source: src/services/otp/forgot-password.service.ts
Đặt lại mật khẩu với cơ chế chống enumeration.
LinkAccountService
Source: src/services/otp/link-account.service.ts
Luồng có xác thực để thêm email hoặc SĐT vào tài khoản đã có.
requestLink({ provider, userId, target })— kiểm tra xung đột (409 nếu target đã có người dùng) rồi gửi OTP với identifier ghépuserId:target, namespacemfa:{provider}:link_account, không cooldown hay giới hạn theo ngày.verifyLink({ provider, sessionToken, code, requestUserId })— verify OTP, kiểm tra chủ sở hữu (result.userId === requestUserId), rồi upsertUserIdentifier.
REST Endpoints
| Endpoint | Auth | Service | Request Body | Response |
|---|---|---|---|---|
POST /auth/phone/request | None | VerifyPhoneService.sendOtp | { phone } | { sessionToken, expiresAt } |
POST /auth/phone/verify | None | VerifyPhoneService.verifyOtp | { sessionToken, otp } | { userId, isNewUser, roles, token } |
POST /auth/email/request | None | VerifyEmailService.sendOtp | { email } | { sessionToken, expiresAt } |
POST /auth/email/verify | None | VerifyEmailService.verifyOtp | { sessionToken, otp } | { userId, isNewUser, roles, token } |
POST /auth/me/phone/request | JWT | LinkAccountService.requestLink('phone') | { phone } | { sessionToken, expiresAt } |
POST /auth/me/phone/verify | JWT | LinkAccountService.verifyLink('phone') | { sessionToken, otp } | { scheme, identifier, verified } |
POST /auth/me/email/request | JWT | LinkAccountService.requestLink('email') | { email } | { sessionToken, expiresAt } |
POST /auth/me/email/verify | JWT | LinkAccountService.verifyLink('email') | { sessionToken, otp } | { scheme, identifier, verified } |
POST /auth/forgot-password/request | None | ForgotPasswordService.requestReset | { email } | { sessionToken, expiresAt } |
POST /auth/forgot-password/verify | None | ForgotPasswordService.verifyCode | { sessionToken, code } | { resetToken, expiresAt } |
POST /auth/forgot-password/reset | None | ForgotPasswordService.resetPassword | { resetToken, newCredential } | { message } |
Rate Limits
| Flow | Namespace | Expiry | Max Attempts | Lockout | Cooldown | Daily Limit |
|---|---|---|---|---|---|---|
| Verify email | mfa:email:verify_email | 10 min | 5 | 15 min | 60 s | 5/day |
| Verify phone | mfa:phone:verify_phone | 5 min | 5 | 10 min | -- | -- |
| Forgot password | mfa:email:forgot_password | 15 min | 5 | 30 min | 60 s | 5/day |
| Link email | mfa:email:link_account | 10 min | 5 | 15 min | -- | -- |
| Link phone | mfa:phone:link_account | 5 min | 5 | 10 min | -- | -- |
Error Codes
| Code | HTTP | Ý nghĩa |
|---|---|---|
CODE_INVALID | 400 | Sai code hoặc session đã hết hạn |
CODE_EXPIRED | 400 | TTL của OTP đã trôi qua |
MAX_ATTEMPTS_EXCEEDED | 429 | Đạt số lần thử tối đa, tài khoản đã bị khóa |
ACCOUNT_LOCKED | 429 | Quá nhiều lần thử thất bại |
RATE_LIMIT_EXCEEDED | 429 | Đụng cooldown hoặc giới hạn theo ngày |
Types
Source: src/services/otp/types.ts
type TOtpProvider = 'email' | 'phone';
type TOtpSessionData = {
identifier: string; // Redis key identifier (email, phone, or userId:target)
provider: TOtpProvider;
target: string; // Actual email or phone number
userId?: string; // Present for link_account flows
};
type TOtpVerifyResult = {
provider: TOtpProvider;
target: string;
userId?: string;
};Trang liên quan
- Authentication -- Luồng sign-in/up, JWKS JWT
- User Management -- UserService, vòng đời identifier
- RBAC -- Phân cấp Role, PolicyDefinition
- Identity Overview -- Kiến trúc, components