9.8 KiB
9.8 KiB
Auth 模組
Gateway 認證領域層:邀請碼、註冊稽核、OAuth Session、CloudEP JWT 簽發/刷新/登出。完整 HTTP 流程由 internal/logic/auth/ 編排,並與 ZITADEL(身份)、Member(會員)、Notification(通知)協作。
架構原則(
docs/model.md§6.1):usecase 為 atomic primitive,不可 呼叫其他 usecase;跨模組編排放internal/logic/auth/。規格 vs 速查:
- 規格書(Data Dictionary
invite_codes/registration_metadata、Redis key、API endpoint list)→SDD.md- 統一註冊(Email/OTP/Social)完整時序 + ZITADEL 互動細節 →
docs/auth-unified-registration.md- 本 README = 範圍 + ServiceContext wiring + 設定 + curl + 快速跟其他模組對齊用
範圍
| 範圍內 | 範圍外(委派) |
|---|---|
| 邀請碼 Validate / Consume | 使用者身份建立 → ZITADEL |
| 註冊稽核 metadata | 會員 profile / OTP → Member |
| OAuth 註冊 / 登入暫存 Session(Redis) | 郵件 / 簡訊 → Notification |
| CloudEP JWT access / refresh 生命週期 | RBAC → Permission |
| JWT 黑名單 + jti pair |
目錄結構
internal/model/auth/
├── README.md # 本檔
├── config/
├── domain/
│ ├── entity/ # InviteCode、RegistrationMetadata
│ ├── enum/ # RegistrationChannel(email / 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 |
完整 schema:generate/api/auth.api;成功 envelope code=102000。
Token(CloudEP JWT)
HS256;access / refresh 使用不同 secret。
| Claim | 說明 |
|---|---|
tenant_id |
租戶 ID |
uid |
會員 UID |
typ |
access 或 refresh |
auth_gen |
簽發世代(強制登出時 +1) |
jti |
Token 唯一 ID |
iat / exp |
簽發 / 過期 |
| Token | 預設 TTL |
|---|---|
| Access | 900s(15 分鐘) |
| Refresh | 604800s(7 天) |
| Registration Session | 600s(10 分鐘) |
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: IssuePair(new 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
│ │
└───────┬────────┘
▼
IssueTokenPair(CloudEP JWT)
Email 路徑
- Logic 驗 tenant_slug + invite + 條款 + 密碼強度 + email 格式
- Logic 消耗 invite(Redis SETNX lock + Mongo
$inc used_count) - ZITADEL
CreateHumanUser member.Lifecycle.CreateUnverified(origin=platform_native)- 寫 registration metadata
member.OTP.Generate(purpose=registration_email)+notifier.Send(verify_registration_email)- 回
{challenge_id, expires_in},不發 JWT POST /register/confirm:OTP verify →Activate→IssueTokenPair
Social 路徑(Google)
- Invite 必填;在 OAuth redirect 前 綁定 registration session(Redis TTL 600s)
- callback 才 消耗 invite(避免 IdP 中途取消)
member.Provisioning.EnsureFromOIDC(同 sub 已存在則視為登入)Member.Registration.TrustSocialEmailVerified=true時,IdPemail_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 組合
- Social:OAuth 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起始即 Consume;Social 在 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+ bytes,prod 走 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.md(TestAuth_*/TestZZZ_AuthTokenRefreshAndLogout);ZITADEL 整合路徑(A-01~A-09)需 staging 環境
相關文件
docs/model.md— Clean Architecture 分層generate/api/README.md—.api+ middleware 約定internal/library/errors/README.md— 8 碼錯誤碼internal/model/member/README.md— Member 模組