Skip to content

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.ts

Base 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

MethodSignatureMô tả
sendsend(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()
verifyByCodeverifyByCode(opts) => TVerifyResultResolve 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
resolveSessionresolveSession(opts) => TSessionDataLấy session data từ Redis theo token

Abstract Members

Subclass phải triển khai:

MemberMục đích
_redisInstance 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 PatternValueTTL
{ns}:otp:{identifier}Code hash, attempts, expiresAtcodeExpirySeconds
{ns}:lock:{identifier}Cờ lockoutlockoutSeconds
{ns}:session:{token}Session data JSONcodeExpirySeconds
{ns}:cooldown:{identifier}Cờ cooldown gửi lạiresendCooldownSeconds
{ns}:daily:{identifier}Bộ đếm yêu cầu trong ngàyĐến nửa đêm

Config Interface

typescript
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 qua IMailService vớ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 qua IOTPSender.
  • _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ọi emailOtp.send() với namespace mfa: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ọi phoneOtp.send() với namespace mfa: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 ghi Customer chư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ép userId:target, namespace mfa:{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 upsert UserIdentifier.

REST Endpoints

EndpointAuthServiceRequest BodyResponse
POST /auth/phone/requestNoneVerifyPhoneService.sendOtp{ phone }{ sessionToken, expiresAt }
POST /auth/phone/verifyNoneVerifyPhoneService.verifyOtp{ sessionToken, otp }{ userId, isNewUser, roles, token }
POST /auth/email/requestNoneVerifyEmailService.sendOtp{ email }{ sessionToken, expiresAt }
POST /auth/email/verifyNoneVerifyEmailService.verifyOtp{ sessionToken, otp }{ userId, isNewUser, roles, token }
POST /auth/me/phone/requestJWTLinkAccountService.requestLink('phone'){ phone }{ sessionToken, expiresAt }
POST /auth/me/phone/verifyJWTLinkAccountService.verifyLink('phone'){ sessionToken, otp }{ scheme, identifier, verified }
POST /auth/me/email/requestJWTLinkAccountService.requestLink('email'){ email }{ sessionToken, expiresAt }
POST /auth/me/email/verifyJWTLinkAccountService.verifyLink('email'){ sessionToken, otp }{ scheme, identifier, verified }
POST /auth/forgot-password/requestNoneForgotPasswordService.requestReset{ email }{ sessionToken, expiresAt }
POST /auth/forgot-password/verifyNoneForgotPasswordService.verifyCode{ sessionToken, code }{ resetToken, expiresAt }
POST /auth/forgot-password/resetNoneForgotPasswordService.resetPassword{ resetToken, newCredential }{ message }

Rate Limits

FlowNamespaceExpiryMax AttemptsLockoutCooldownDaily Limit
Verify emailmfa:email:verify_email10 min515 min60 s5/day
Verify phonemfa:phone:verify_phone5 min510 min----
Forgot passwordmfa:email:forgot_password15 min530 min60 s5/day
Link emailmfa:email:link_account10 min515 min----
Link phonemfa:phone:link_account5 min510 min----

Error Codes

CodeHTTPÝ nghĩa
CODE_INVALID400Sai code hoặc session đã hết hạn
CODE_EXPIRED400TTL của OTP đã trôi qua
MAX_ATTEMPTS_EXCEEDED429Đạt số lần thử tối đa, tài khoản đã bị khóa
ACCOUNT_LOCKED429Quá nhiều lần thử thất bại
RATE_LIMIT_EXCEEDED429Đụng cooldown hoặc giới hạn theo ngày

Types

Source: src/services/otp/types.ts

typescript
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

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.