template-monorepo/internal/model/auth
王性驊 9dd8287777 add fix eage case 2026-05-27 17:28:13 +08:00
..
config feat(auth): add unified registration/login module with Zitadel + lint cleanup 2026-05-21 14:45:35 +08:00
domain add fix eage case 2026-05-27 17:28:13 +08:00
repository feat(auth): 登入 MFA、忘記/改密碼與註冊恢復流程 2026-05-27 00:55:37 +08:00
usecase feat(auth): 登入 MFA、忘記/改密碼與註冊恢復流程 2026-05-27 00:55:37 +08:00
README.md docs: 統整模組 README ↔ SDD 分工,砍重複內容 2026-05-22 17:18:08 +08:00
SDD.md add member totp 2026-05-22 07:52:39 +08:00

README.md

Auth 模組

Gateway 認證領域層邀請碼、註冊稽核、OAuth Session、CloudEP JWT 簽發/刷新/登出。完整 HTTP 流程由 internal/logic/auth/ 編排,並與 ZITADEL身份、Member會員、Notification通知協作。

架構原則docs/model.md §6.1usecase 為 atomic primitive不可 呼叫其他 usecase跨模組編排放 internal/logic/auth/

規格 vs 速查

  • 規格書Data Dictionary invite_codes / registration_metadata、Redis key、API endpoint listSDD.md
  • 統一註冊Email/OTP/Social完整時序 + ZITADEL 互動細節 → docs/auth-unified-registration.md
  • 本 README = 範圍 + ServiceContext wiring + 設定 + curl + 快速跟其他模組對齊用

範圍

範圍內 範圍外(委派)
邀請碼 Validate / Consume 使用者身份建立 → ZITADEL
註冊稽核 metadata 會員 profile / OTP → Member
OAuth 註冊 / 登入暫存 SessionRedis 郵件 / 簡訊 → Notification
CloudEP JWT access / refresh 生命週期 RBAC → Permission
JWT 黑名單 + jti pair

目錄結構

internal/model/auth/
├── README.md                # 本檔
├── config/
├── domain/
│   ├── entity/              # InviteCode、RegistrationMetadata
│   ├── enum/                # RegistrationChannelemail / google
│   ├── repository/          # 介面 + Session struct
│   ├── usecase/             # 介面 + DTO
│   ├── const.go             # BSON 欄位、Redis key、code hash helper
│   └── errors.go            # 領域 sentinel
├── repository/              # Mongo + Redis 實作 + EnsureMongoIndexes
└── usecase/                 # 實作 + NewModuleFromParam

Module 與依賴

flowchart TB
    Logic["logic/auth\n(orchestration)"]
    subgraph M["auth.Module"]
      Token[TokenUseCase]
      Invite[InviteUseCase]
      RegMeta[RegistrationMetaUseCase]
      RegSess[RegistrationSessionUseCase]
      LoginSess[LoginSessionUseCase]
    end
    Logic --> Token
    Logic --> Invite
    Logic --> RegMeta
    Logic --> RegSess
    Logic --> LoginSess
    Logic --> Member[(member.Module)]
    Logic --> Zitadel[(library/zitadel)]
    Logic --> Notif[(notification)]

ServiceContext 注入:

欄位 UseCase 啟用條件
AuthToken TokenUseCase JWT secret + Redis
AuthInvite InviteUseCase Mongo + Redis
AuthRegistrationMeta RegistrationMetaUseCase Mongo
AuthRegistrationSession RegistrationSessionUseCase Redis
AuthLoginSession LoginSessionUseCase Redis

API/api/v1/auth

公開(無 Bearer

Method Path 說明
POST /register Email 註冊 → {challenge_id, expires_in, uid}
POST /register/confirm OTP 確認 → 核發 JWT
POST /register/resend 重發註冊 OTP
POST /register/social/start 社交註冊起始 → {oauth_url, session_id}
GET /register/social/callback OAuth callback → JWT
POST /login 密碼登入ZITADEL ROPG
POST /login/social/start 社交登入起始
GET /login/social/callback 社交登入 callback
POST /token/refresh 刷新 token pair
POST /token/exchange id_token → CloudEP JWT

需 Bearer

Method Path 說明
POST /logout 黑名單 access + paired refresh jti

完整 schemagenerate/api/auth.api;成功 envelope code=102000


TokenCloudEP JWT

HS256access / refresh 使用不同 secret。

Claim 說明
tenant_id 租戶 ID
uid 會員 UID
typ accessrefresh
auth_gen 簽發世代(強制登出時 +1
jti Token 唯一 ID
iat / exp 簽發 / 過期
Token 預設 TTL
Access 900s15 分鐘)
Refresh 604800s7 天)
Registration Session 600s10 分鐘)

Redis Key

Key 用途 TTL
auth:jwt:pair:{jti} access ↔ refresh jti 映射 token TTL
auth:jwt:bl:{jti} 黑名單 jti 至自然過期
auth:register:session:{id} 社交註冊 OAuth session 600s
auth:login:session:{id} 社交登入 OAuth session 600s
auth:invite:consume:{tenant}:{hash} 邀請碼消費鎖 30s

Refresh 流程

sequenceDiagram
    participant C as Client
    participant L as TokenRefreshLogic
    participant T as TokenUseCase
    participant R as Redis
    C->>L: POST /auth/token/refresh {refresh_token}
    L->>T: Refresh(refreshToken)
    T->>T: Parse + verify typ=refresh
    T->>R: Blacklist 舊 refresh jti + paired access jti
    T->>T: IssuePairnew access + refresh
    T->>R: SavePair(new jti mapping)
    T-->>L: TokenPair
    L-->>C: AuthTokenData

統一註冊Email + Social

取代原 ZITADEL Hosted Page使用者只與 Gateway 互動。Invite 為 B2B 必填、B2C 可選(Member.Registration.RequireInviteCode)。

三條路徑

┌─────────────────────────────────────────────┐
│         前端「註冊」頁(共用 invite + 條款)  │
└────────────┬────────────────┬───────────────┘
             ▼                ▼
    Email + Password     Social (Google)
             │                │
   POST /register       POST /register/social/start
             │                │ → oauth_url
   POST /register/confirm   GET /register/social/callback
             │                │
             └───────┬────────┘
                     ▼
             IssueTokenPairCloudEP JWT

Email 路徑

  1. Logic 驗 tenant_slug + invite + 條款 + 密碼強度 + email 格式
  2. Logic 消耗 inviteRedis SETNX lock + Mongo $inc used_count
  3. ZITADEL CreateHumanUser
  4. member.Lifecycle.CreateUnverifiedorigin=platform_native
  5. 寫 registration metadata
  6. member.OTP.Generate(purpose=registration_email) + notifier.Send(verify_registration_email)
  7. {challenge_id, expires_in}不發 JWT
  8. POST /register/confirmOTP verify → ActivateIssueTokenPair

Social 路徑Google

  • Invite 必填;在 OAuth redirect 綁定 registration sessionRedis TTL 600s
  • callback 才 消耗 invite避免 IdP 中途取消)
  • member.Provisioning.EnsureFromOIDC(同 sub 已存在則視為登入)
  • Member.Registration.TrustSocialEmailVerified=trueIdP email_verified=true 直接 Activate,否則走 OTP

失敗補償

步驟 補償
Invite 無效 直接 4xx無 side effect
ZITADEL 建 user OK / member 失敗 Logic 呼叫 zitadel.DeactivateUser(sub)
Member OK / Send OTP 失敗 OTP.Invalidate(challenge_id)
confirm 時 Activate 失敗 5xx保留 challenge 可重試

Invite 在 ZITADEL/member 失敗時 不自動回滾(防刷;可 admin 補發)。

Logic 商務驗證一覽(不下沉 usecase

  • invite 必填 / 過期 / 限新 user
  • 條款版本接受
  • tenant 是否允許 B2C 註冊
  • 密碼政策
  • 註冊 rate limit key 組合
  • SocialOAuth state 與 registration session 綁定

Invite Code

欄位 說明
tenant_id 租戶
code_hash SHA-256(normalized code)永不明文
max_uses / used_count 總次數 / 已用
expires_at 0 = 永不過期
new_users_only 限新用戶(社交註冊用)
  • 索引:(tenant_id, code_hash) unique
  • Validate / Consume 介面:InviteUseCase
  • Email 註冊在 /register 起始即 ConsumeSocial 在 Callback 才 Consume

Rate Limit建議由 middleware 落地)

Key 限制
auth:register:ip:{ip} 10 / hour
auth:register:email:{tenant}:{email} 3 / hour
auth:register:invite_fail:{ip} 20 / hour

OTP resend 沿用 member.VerifyRate


錯誤碼Auth scope 28

情境 errb
invite 無效 InputInvalidFormat / ResNotFound("invite")
invite 用盡 ResInsufficientQuota
email 已註冊 ResAlreadyExist
OTP 錯誤 AuthForbidden + cause
tenant 不允許註冊 AuthForbidden
未接受條款 InputMissingRequired
ZITADEL 下游失敗 SvcThirdParty
DB 失敗 DBError via wrapRepoErr

Scope / category 完整對照:internal/library/errors/README.md


設定(etc/gateway.dev.yaml

Auth:
  AccessExpire: 900
  RefreshExpire: 604800
  ActiveKID: v1
  AccessSecret: ...   # 32+ bytesprod 走 env / secret manager
  RefreshSecret: ...
  RegistrationSessionTTLSeconds: 600

Zitadel:
  Issuer: https://zitadel.internal
  ServiceUserToken: ${ZITADEL_SERVICE_TOKEN}
  GoogleClientID: ...
  GoogleClientSecret: ...
  DefaultOrgID: ...

測試

  • 單元go test ./internal/model/auth/...invite consume 並發、JWT parse/refresh、errb 映射)
  • E2E:見 docs/e2e-testing.mdTestAuth_* / TestZZZ_AuthTokenRefreshAndLogoutZITADEL 整合路徑A-01~A-09需 staging 環境

相關文件