292 lines
9.8 KiB
Markdown
292 lines
9.8 KiB
Markdown
|
|
# Auth 模組
|
|||
|
|
|
|||
|
|
Gateway 認證領域層:邀請碼、註冊稽核、OAuth Session、CloudEP JWT 簽發/刷新/登出。完整 HTTP 流程由 `internal/logic/auth/` 編排,並與 ZITADEL(身份)、Member(會員)、Notification(通知)協作。
|
|||
|
|
|
|||
|
|
> **架構原則**([`docs/model.md`](../../../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`](./SDD.md)
|
|||
|
|
> - 統一註冊(Email/OTP/Social)完整時序 + ZITADEL 互動細節 → [`docs/auth-unified-registration.md`](../../../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 與依賴
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
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 流程
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
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 路徑
|
|||
|
|
|
|||
|
|
1. Logic 驗 tenant_slug + invite + 條款 + 密碼強度 + email 格式
|
|||
|
|
2. Logic 消耗 invite(Redis SETNX lock + Mongo `$inc used_count`)
|
|||
|
|
3. ZITADEL `CreateHumanUser`
|
|||
|
|
4. `member.Lifecycle.CreateUnverified`(`origin=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/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`](../../library/errors/README.md)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 設定(`etc/gateway.dev.yaml`)
|
|||
|
|
|
|||
|
|
```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`](../../../docs/e2e-testing.md)(`TestAuth_*` / `TestZZZ_AuthTokenRefreshAndLogout`);ZITADEL 整合路徑(A-01~A-09)需 staging 環境
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 相關文件
|
|||
|
|
|
|||
|
|
- [`docs/model.md`](../../../docs/model.md) — Clean Architecture 分層
|
|||
|
|
- [`generate/api/README.md`](../../../generate/api/README.md) — `.api` + middleware 約定
|
|||
|
|
- [`internal/library/errors/README.md`](../../library/errors/README.md) — 8 碼錯誤碼
|
|||
|
|
- [`internal/model/member/README.md`](../member/README.md) — Member 模組
|