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 時,IdP 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 組合
- 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 環境
相關文件