template-monorepo/docs/identity-member-design.md

2628 lines
107 KiB
Markdown
Raw Normal View History

# Identity / Member / Permission <20>Ҳճ]<5D>p<EFBFBD><70><EFBFBD>Z
2026-05-19 13:56:59 +00:00
> **<2A><><EFBFBD>A**<2A>GDraft<66>]<5D><> Review<65>^
> **<2A>A<EFBFBD>αM<CEB1><4D>**<2A>GPortal API Gateway<61>]PGW<47>^
> **<2A>Ѧҹ<D1A6><D2B9>@**<2A>G[app-cloudep-permission-server](https://code.30cm.net/digimon/app-cloudep-permission-server)<29>]Casbin RBAC<41>BPermission Tree<65>BRole/RolePermission<6F>^
> **<2A>̫<EFBFBD><CCAB><EFBFBD><EFBFBD>s**<2A>G2026-05-19
> **<2A>e<EFBFBD><65>**<2A>G<EFBFBD><47><EFBFBD>s Gateway module<6C>A<EFBFBD><41><EFBFBD>Ҽ{<7B>ª<EFBFBD> member-server <20>E<EFBFBD><45><EFBFBD>C
2026-05-19 13:56:59 +00:00
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>y<EFBFBD>z Gateway <20><> **auth**<EFBFBD>B**member**<2A>B**permission** <20>T<EFBFBD>ӷ~<7E>ȼҲժ<D2B2><D5AA>ؼЬ[<5B>c<EFBFBD>A<EFBFBD><41><EFBFBD>X **ZITADEL**<EFBFBD>]<5D><><EFBFBD><EFBFBD><EFBFBD>^<5E>B**LDAP**<2A>]<5D><><EFBFBD>~<7E>ؿ<EFBFBD><D8BF>^<5E>B**SCIM 2.0**<2A>]<5D><><EFBFBD>~ provisioning<6E>^<5E>A<EFBFBD>**<EFBFBD>h<EFBFBD><EFBFBD><EFBFBD><EFBFBD>** <20>P **<EFBFBD>ʸU<EFBFBD>ŷ|<7C><>**<2A>]<5D>t<EFBFBD><EFBFBD><E6AFB2> 50 <20>U<EFBFBD>^<5E>C
2026-05-19 13:56:59 +00:00
<EFBFBD>Ҳդ<EFBFBD><EFBFBD>h<EFBFBD>P<EFBFBD>{<7B><><EFBFBD>X<EFBFBD><58><EFBFBD>g<EFBFBD>W<EFBFBD>d<EFBFBD><64> [model.md](./model.md)<29>C
2026-05-19 13:56:59 +00:00
---
## <20>ؿ<EFBFBD>
1. [<5B>]<5D>p<EFBFBD>ؼлP<D0BB><50><EFBFBD>h](#1-<2D>]<5D>p<EFBFBD>ؼлP<D0BB><50><EFBFBD>h)
2. [<EFBFBD>Ҳե<EFBFBD><EFBFBD><EFBFBD>](#2-<2D>Ҳե<D2B2><D5A5><EFBFBD>)
3. [<EFBFBD>~<7E><><EFBFBD>t<EFBFBD>Τ<EFBFBD><CEA4>u](#3-<2D>~<7E><><EFBFBD>t<EFBFBD>Τ<EFBFBD><CEA4>u)
4. [auth <20>Ҳ<EFBFBD>](#4-auth-<2D>Ҳ<EFBFBD>)
5. [member <20>Ҳ<EFBFBD>](#5-member-<2D>Ҳ<EFBFBD>)
6. [permission <20>Ҳա]B2B <20>۩w<DBA9>q<EFBFBD>^](#6-permission-<2D>Ҳ<EFBFBD>b2b-<2D>۩w<DBA9>q)
7. [API <20>W<EFBFBD><57>](#7-api-<2D>W<EFBFBD><57>)
8. [Middleware <20><>](#8-middleware-<2D><>)
9. [<EFBFBD>֤߬y<EFBFBD>{](#9-<2D>֤߬y<DFAC>{)
10. [LDAP <20>P SCIM](#10-ldap-<2D>P-scim)
2026-05-19 17:04:26 +00:00
11. [Notification Module](#11-notification-module)
12. [<5B>iŪ UID <20>]<5D>p](#12-<2D>iŪ-uid-<2D>]<5D>p<EFBFBD>w<EFBFBD>M<EFBFBD><4D>)
13. [<EFBFBD><EFBFBD><EFBFBD>Ƽҫ<EFBFBD><EFBFBD>P<EFBFBD><EFBFBD><EFBFBD><EFBFBD>](#13-<2D><><EFBFBD>Ƽҫ<C6BC><D2AB>P<EFBFBD><50><EFBFBD><EFBFBD>)
14. [Redis Key <20>R<EFBFBD>W](#14-redis-key-<2D>R<EFBFBD>W)
15. [<5B>W<EFBFBD>һP<D2BB>ʯ<EFBFBD><CAAF>]100 <20>U+ / <20><EFBFBD><E6AFB2> 50 <20>U<EFBFBD>^](#15-<2D>W<EFBFBD>һP<D2BB>ʯ<EFBFBD>100-<2D>U--<2D><EFBFBD><E6AFB2>-50-<2D>U)
16. [<EFBFBD>ؿ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>c](#16-<2D>ؿ<EFBFBD><D8BF><EFBFBD><EFBFBD>c)
17. [<5B>]<5D>w<EFBFBD><77>](#17-<2D>]<5D>w<EFBFBD><77>)
18. [<EFBFBD><EFBFBD><EFBFBD>I<EFBFBD><EFBFBD><EFBFBD><EFBFBD>](#18-<2D><><EFBFBD>I<EFBFBD><49><EFBFBD><EFBFBD>)
19. [<EFBFBD>w<EFBFBD>M<EFBFBD><EFBFBD><EFBFBD>ƶ<EFBFBD>](#19-<2D>w<EFBFBD>M<EFBFBD><4D><EFBFBD>ƶ<EFBFBD>)
20. [Audit Log <20>P Rate Limit](#20-audit-log-<2D>P-rate-limit)
2026-05-19 13:56:59 +00:00
---
## 1. <20>]<5D>p<EFBFBD>ؼлP<D0BB><50><EFBFBD>h
2026-05-19 13:56:59 +00:00
### 1.1 <20>ؼ<EFBFBD>
2026-05-19 13:56:59 +00:00
| <20>ؼ<EFBFBD> | <20><><EFBFBD><EFBFBD> |
2026-05-19 13:56:59 +00:00
|------|------|
| <20>Τ@<40><><EFBFBD><EFBFBD> | ZITADEL <20>@<40><> IdP<64>]<5D>t LDAP IdP<64>BSocial Login<69>^ |
| <20>~<7E>ȷ|<7C><> | Gateway `member` <20>Ҳպ޲z tenant-scoped profile |
| <20>Ӳɫױ<C9AB><D7B1>v | Gateway `permission` <20>Ҳա]**Casbin RBAC + Permission Tree**<2A>^<5E>F**<2A>C<EFBFBD><43> B2B <20><><EFBFBD><EFBFBD><EFBFBD>i<EFBFBD>۩w<DBA9>q Role <20>äĿ<C3A4> Permission** |
| Token | go-zero JWT <20><><EFBFBD><EFBFBD> + Redis <20>¦W<C2A6><57><EFBFBD>]<5D>u<EFBFBD>¦W<C2A6><57> JWT<57>^ |
| <20><><EFBFBD>~<7E><><EFBFBD>X | SCIM 2.0 + LDAP Directory Sync<6E>]AD + OpenLDAP<41>^ |
| <20>W<EFBFBD><57> | <20><><EFBFBD><EFBFBD><EFBFBD>x 100 <20>U+ <20>|<7C><><EFBFBD>F<EFBFBD><EFBFBD><E6AFB2><EFBFBD>i<EFBFBD>F 50 <20>U |
| UID | <20>H<EFBFBD><48><EFBFBD><69>B<EFBFBD>a<EFBFBD><61><EFBFBD><EFBFBD><EFBFBD>e<EFBFBD><65><EFBFBD>A<EFBFBD>p `AMEX-10000000`<EFBFBD>F<EFBFBD>ߤ@<40>ʥH `tenant_id + uid` <20><><EFBFBD><EFBFBD> |
2026-05-19 13:56:59 +00:00
### 1.2 <20>֤߭<D6A4><DFAD>h
2026-05-19 13:56:59 +00:00
1. **¾<EFBFBD>d<EFBFBD><EFBFBD><EFBFBD><EFBFBD>**
- `auth`<EFBFBD>G<EFBFBD>A<EFBFBD>O<EFBFBD>֡]Authentication<6F>^
- `member`<EFBFBD>G<EFBFBD>A<EFBFBD><EFBFBD><EFBFBD>~<7E>ȸ<EFBFBD><C8B8>ƬO<C6AC><4F><EFBFBD><EFBFBD><EFBFBD>]Profile<6C>^
- `permission`<EFBFBD>G<EFBFBD>A<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]Authorization<6F>^
2026-05-19 13:56:59 +00:00
2. **LDAP <20><><EFBFBD><EFBFBD><EFBFBD>n<EFBFBD>J bind**
- <20>n<EFBFBD>J<EFBFBD><4A><EFBFBD>ҥ<EFBFBD> ZITADEL LDAP IdP <20>B<EFBFBD>z
- Gateway <20><> LDAP client <20>Ȩ<EFBFBD> Directory Sync<6E>]read-only<6C>^
2026-05-19 13:56:59 +00:00
3. **Token Exchange**
- <20><><EFBFBD>~ API <20>u<EFBFBD><75><EFBFBD><EFBFBD> Gateway ñ<>o<EFBFBD><6F> CloudEP JWT
- ZITADEL OIDC token <20>Ȧb `/auth/token/exchange` <20>ϥΤ@<40><>
4. **<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>j<EFBFBD><EFBFBD>**
- <20>Ҧ<EFBFBD><D2A6><EFBFBD><EFBFBD>[<5B>Ƹ<EFBFBD><C6B8>ƥH `tenant_id` <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
- JWT `tenant_id` <20>P<EFBFBD>ШD<D0A8><EFBFBD><EAB7BD><EFBFBD><EFBFBD><EFBFBD>@<40>P
5. **B2B <20>v<EFBFBD><76><EFBFBD>۩w<DBA9>q**<EFBFBD>]<5D>Ѧ<EFBFBD> [app-cloudep-permission-server](https://code.30cm.net/digimon/app-cloudep-permission-server)<29>^
- <20><><EFBFBD>x seed <20><><EFBFBD><EFBFBD> Permission Tree<65>]<5D>t `http_path` / `http_method`<EFBFBD>^
- <20><><EFBFBD><EFBFBD><EFBFBD>إߦۭq Role<6C>A<EFBFBD>q Tree **<EFBFBD>Ŀ<EFBFBD>** Permission<6F>]`RolePermission` + <20>۰ʸ<DBB0> parent<6E>^
- API <20><><EFBFBD>v<EFBFBD><76> **Casbin** <20><><EFBFBD><EFBFBD> `(tenant_id, role_key, path, method)`<EFBFBD>A<EFBFBD>קK<EFBFBD><EFBFBD><EFBFBD>P<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>P<EFBFBD>W<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ۦìV
- B2C <20><><EFBFBD><EFBFBD>**<2A><>Ū** seed <20>ҪO<D2AA>A**<2A><><EFBFBD>i**<2A>۩w<DBA9>q Role<6C>]<5D>w<EFBFBD>M<EFBFBD><4D><EFBFBD>^
6. **<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> vs <20>~<7E><><EFBFBD><EFBFBD><EFBFBD>Ҥ<EFBFBD><D2A4>h**<2A>]<5D>w<EFBFBD>M<EFBFBD><4D><EFBFBD>^
- **ZITADEL = <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>**<2A>G<EFBFBD>n<EFBFBD>J MFA<46>]TOTP / WebAuthn / SMS<4D>^<5E>B<EFBFBD><42><EFBFBD>U email <20><><EFBFBD>ҡB<D2A1>ѰO<D1B0>K<EFBFBD>X<EFBFBD>B<EFBFBD>b<EFBFBD><62><EFBFBD><EFBFBD><EFBFBD>w
- **Gateway member = <20>~<7E>ȯ<EFBFBD><C8AF><EFBFBD><EFBFBD><EFBFBD>**<2A>G<EFBFBD>~<7E><> email / phone <20>j<EFBFBD>w OTP<54>BStep-up MFA
- Gateway **<EFBFBD><EFBFBD>**<2A>̿<EFBFBD> `ZITADEL email_verified` <20><><EFBFBD>~<7E>Ȧu<C8A6><75><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>FLogic <20>h<EFBFBD><68>Ū `BusinessEmailVerified` <20><> member <20>X<EFBFBD><58>
- **Email / SMS OTP <20><> Gateway <20>۰e**<2A>]<5D><><EFBFBD><EFBFBD> ZITADEL Notification<6F>^
- **MFA <20>j<EFBFBD><EFBFBD><EEB5A6>**<2A>Gadmin <20><> role <20><> ZITADEL Org Policy <20>j<EFBFBD><6A> TOTP<54>F<EFBFBD>@<40><> user <20>w<EFBFBD>]<5D><><EFBFBD>j<EFBFBD><6A><EFBFBD>A<EFBFBD><41><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>I<EFBFBD>ާ@<40><> Gateway Step-up
2026-05-19 13:56:59 +00:00
---
## 2. <20>Ҳե<D2B2><D5A5><EFBFBD>
```
<EFBFBD>z<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>{
<EFBFBD>x Portal API Gateway (go-zero) <20>x
<EFBFBD>u<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>t
<EFBFBD>x generate/api/ <20>x
<EFBFBD>x auth.api <20>P member.api <20>P permission.api <20>P tenant.api <20>P scim.api<70>x
<EFBFBD>u<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>t
<EFBFBD>x internal/middleware/ <20>x
<EFBFBD>x jwt_revoke <20>P casbin_rbac <20>P scim_auth <20>P tenant_context <20>x
<EFBFBD>u<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>t
<EFBFBD>x internal/model/ <20>x
<EFBFBD>x auth/ <20><> Token ñ<>o<EFBFBD>B<EFBFBD><42><EFBFBD><EFBFBD><EFBFBD>B<EFBFBD>n<EFBFBD>X<EFBFBD>B<EFBFBD>¦W<C2A6><57><EFBFBD>Bauth_gen<65>Bstep-up<75>x
<EFBFBD>x member/ <20><> Profile<6C>BIdentity<74>BTenant<6E>BUID<49>BSync<6E>BTOTP<54>B<EFBFBD><42><EFBFBD>Ңx
<EFBFBD>x permission/ <20><> Casbin RBAC<41>BPermission Tree<65>BRole<6C>]B2B <20>۩w<DBA9>q<EFBFBD>^<5E>x
<EFBFBD>x notification/ <20><> Email/SMS/Push <20>Τ@<40>o<EFBFBD>e<EFBFBD>B<EFBFBD>ҪO<D2AA>B<EFBFBD><42><EFBFBD>աBaudit <20>x
<EFBFBD>u<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>t
<EFBFBD>x internal/library/ <20>x
<EFBFBD>x zitadel/ <20>P ldap/ <20>P uid/ <20>P casbin/ <20>x
<EFBFBD>x notification/email <20>P notification/sms <20>P notification/push <20>x
<EFBFBD>u<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>t
<EFBFBD>x internal/worker/ <20>x
<EFBFBD>x directory_sync/ <20>P notification_retry/ <20>P member_anonymize/ <20>x
<EFBFBD>|<7C>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>}
<20>x <20>x <20>x
<20><> <20><> <20><>
2026-05-19 13:56:59 +00:00
MongoDB Redis ZITADEL
(profile/role) (cache/blacklist) (identity/LDAP IdP)
2026-05-19 17:04:26 +00:00
+
Email / SMS Provider
2026-05-19 13:56:59 +00:00
```
### 2.1 <20>Ҳը̿<D5A8><CCBF><EFBFBD><EFBFBD>V
2026-05-19 13:56:59 +00:00
```
handler <20><> logic <20><> model/{auth|member|permission|notification}/usecase<73>]interface<63>^
<20><>
repository <20><> MongoDB / Redis
2026-05-19 13:56:59 +00:00
logic <20><> import entity / repository<72>]<5D><> model.md<6D>^
2026-05-19 13:56:59 +00:00
auth <20><> member<65>]EnsureFromOIDC / EnsureFromLDAP / EnsureFromSCIM<49>^
auth <20><> permission<6F>]SyncRolesFromClaims<6D>^
auth <20><> member.TOTPUseCase<73>]step-up TOTP <20><><EFBFBD>ҡ^
member <20><> auth<74>]<5D><><EFBFBD>v<EFBFBD><76> RevokeAllForUser<65>^
member <20><> notification<6F>]<5D>~<7E><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> / step-up OTP <20>H<EFBFBD>e<EFBFBD>^
permission <20><> member<65>]<5D>i<EFBFBD><69><EFBFBD>G<EFBFBD><47><EFBFBD><EFBFBD> uid <20>s<EFBFBD>b<EFBFBD>^
notification <20><> library/notification/{email,sms,push}<7D>]provider <20><><EFBFBD>@<40>^
2026-05-19 13:56:59 +00:00
```
---
## 3. <20>~<7E><><EFBFBD>t<EFBFBD>Τ<EFBFBD><CEA4>u
2026-05-19 13:56:59 +00:00
| <20><><EFBFBD>O | ZITADEL | Gateway auth | Gateway member | Gateway permission | Gateway notification |
2026-05-19 17:04:26 +00:00
|------|---------|--------------|----------------|-------------------|----------------------|
| <20><><EFBFBD>U / <20>n<EFBFBD>J<EFBFBD>]OIDC / LDAP / SCIM<49>^ | ? | <20><><EFBFBD><EFBFBD> | EnsureFromOIDC/LDAP/SCIM | SyncRoles | <20>X |
| <20><><EFBFBD>x<EFBFBD><78><EFBFBD>͵<EFBFBD><CDB5>U<EFBFBD>]<5D><><EFBFBD>ӡA<D3A1>t email OTP<54>^ | <20>]local user<65>^| <20>X | LifecycleUseCase + OTPUseCase | <20>X | <20>H OTP |
| <20>K<EFBFBD>X / <20><><EFBFBD><EFBFBD> MFA / <20>ѰO<D1B0>K<EFBFBD>X | ? | <20>X | <20>X | <20>X | <20>X |
| <20><><EFBFBD><EFBFBD> MFA <20>j<EFBFBD><EFBFBD><EEB5A6> | ? Org Policy | <20>X | <20>X | <20>X | <20>X |
| Google / LINE / Apple | ? IdP | <20>X | <20>X | <20>X | <20>X |
| LDAP <20>n<EFBFBD>J | ? LDAP IdP | <20>X | <20>X | Group<75><70>Role <20>M<EFBFBD>g | <20>X |
| Access / Refresh Token<65>]<5D><><EFBFBD>~<7E>^ | <20>X | ? CloudEP JWT | <20>X | <20>X | <20>X |
| Step-up Token<65>]<5D><><EFBFBD><EFBFBD><EFBFBD>I<EFBFBD>ާ@<40>^ | <20>X | ? ñ step_up_token | OTP / TOTP <20><><EFBFBD><EFBFBD> | Logic <20>u<EFBFBD><75> | OTP <20>H<EFBFBD>e |
| <20>~<7E><> TOTP<54>]Authenticator<6F>^ | <20>X | <20>X | ? secret <20>[<5B>K<EFBFBD>x<EFBFBD>s + <20><><EFBFBD><EFBFBD> | <20>X | <20>X |
| JWT <20>¦W<C2A6><57> | <20>X | ? Redis | <20>X | <20>X | <20>X |
| <20>~<7E><> UID | <20>X | <20>X | ? | <20>X | <20>X |
| Profile | <20>X | <20>X | ? | <20>X | <20>X |
| <20>~<7E><> Email / Phone <20><><EFBFBD><EFBFBD> | <20>X | <20>X | ? Verification <20>y<EFBFBD>{ | <20>X | ? OTP <20>H<EFBFBD>e |
| Email / SMS / Push <20>o<EFBFBD>e | <20>X | <20>X | <20>X | <20>X | ? <20>Τ@<40>J<EFBFBD>f + <20>ҪO + <20><><EFBFBD><EFBFBD> |
| <20>|<7C><><EFBFBD>C<EFBFBD><43> / <20><><EFBFBD>A | <20>X | <20>X | ? | <20>ݱ<EFBFBD><DDB1>v | <20>ܧ<EFBFBD><DCA7>q<EFBFBD><71><EFBFBD>]<5D><><EFBFBD>B<EFBFBD>^ |
| API <20>Ӳɫ<D3B2><C9AB>v<EFBFBD><76> | <20>ʲɫ<CAB2> Role | <20>X | <20>X | **Casbin RBAC**<EFBFBD>]path + method<6F>^ | <20>X |
| SCIM Users/Groups | <20>i<EFBFBD>P<EFBFBD>B | <20>X | ? <20>~<7E>ȼg<C8BC>J | ? Group<75><70>Role | <20>X |
| LDAP Directory Sync | <20>X | <20>X | ? Worker | ? Group<75><70>Role | <20>P<EFBFBD>B<EFBFBD><42><EFBFBD>`<60>iĵ |
### 3.1 <20>h<EFBFBD><68><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
```
1 CloudEP Tenant = 1 ZITADEL Organization = 1 <20><><EFBFBD>ƹj<C6B9><6A><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
```
| <20><><EFBFBD><EFBFBD> | <20>ӷ<EFBFBD> | <20>γ~ |
2026-05-19 13:56:59 +00:00
|------|------|------|
| `tenant_id` | ZITADEL `org_id` | <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>B<EFBFBD><42><EFBFBD>v<EFBFBD><76><EFBFBD><EFBFBD> |
| `identity_id` | ZITADEL `sub` | <20><><EFBFBD><EFBFBD><EFBFBD>M<EFBFBD>g |
| `uid` | Member <20>Ҳղ<D2B2><D5B2><EFBFBD> | <20>~<7E>ȷ|<7C><> ID<49>]<5D>p `AMEX-10000000`<EFBFBD>^ |
2026-05-19 17:04:26 +00:00
#### Tenant <20>إ߶<D8A5><DFB6>ǡ]<5D>w<EFBFBD>M<EFBFBD><4D><EFBFBD>GGateway <20><><EFBFBD>د<EFBFBD><D8AF>Z<EFBFBD>^
2026-05-19 17:04:26 +00:00
```
1. POST /api/v1/admin/tenants { slug, uid_prefix, type, ... }
<20><> Mongo upsert tenants {status: "provisioning", org_id: ""}
2026-05-19 17:04:26 +00:00
2. ZITADEL Mgmt.CreateOrganization(name=slug)
<20><> <20><><EFBFBD><EFBFBD> org_id
2026-05-19 17:04:26 +00:00
3. UPDATE tenants {org_id, status: "active"}
4. seed <20>w<EFBFBD>] Role + Casbin policy reload
5. <20>^<5E><> tenant payload
<EFBFBD><EFBFBD><EFBFBD>Ѹ<EFBFBD><EFBFBD>v<EFBFBD>G
- <20>B<EFBFBD>J 2 <20><><EFBFBD><EFBFBD> <20><> status = "failed"<22>Acron <20><><EFBFBD>ա]<5D><><EFBFBD>ưh<C6B0>סA3 <20><><EFBFBD><EFBFBD><EFBFBD>H<EFBFBD>u<EFBFBD><75><EFBFBD>J<EFBFBD>^
- <20>B<EFBFBD>J 3 <20><><EFBFBD><EFBFBD> <20><> status = "orphan_zitadel_org"<22>Acron <20><><EFBFBD><EFBFBD><EFBFBD>øɸj
2026-05-19 17:04:26 +00:00
```
> Saga <20><><EFBFBD><EFBFBD><EFBFBD>GGateway <20><><EFBFBD>D<EFBFBD>BZITADEL <20><><EFBFBD>q<EFBFBD>F<EFBFBD><46><EFBFBD>v cron <20>C 5 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> `status in ("failed", "orphan_zitadel_org")` <20><><EFBFBD>թΧ<69>C
2026-05-19 13:56:59 +00:00
### 3.2 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
2026-05-19 13:56:59 +00:00
| <20><><EFBFBD><EFBFBD> | <20>n<EFBFBD>J | LDAP | <20>v<EFBFBD><76> |
2026-05-19 13:56:59 +00:00
|------|------|------|------|
| **B2C** | Email / Social | <20>L | <20>t<EFBFBD>ιw<CEB9>] Role<6C>]<5D><><EFBFBD>i<EFBFBD>Τ<EFBFBD><CEA4>`<60>۩w<DBA9>q<EFBFBD>^ |
| **B2B** | ZITADEL <20><> LDAP IdP | <20><> | **<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>۩w<EFBFBD>q Role + Permission** |
| **Hybrid** | Social + LDAP | <20><> | B2B <20>۩w<DBA9>q<EFBFBD>F<EFBFBD>~<7E><><EFBFBD>Ȥ<EFBFBD><C8A4><EFBFBD> B2C <20><>Ū<EFBFBD>ҪO |
2026-05-19 17:04:26 +00:00
### 3.3 ZITADEL <20><><EFBFBD>p<EFBFBD>]<5D>w<EFBFBD>M<EFBFBD><4D><EFBFBD>GSelf-hosted<65>^
2026-05-19 17:04:26 +00:00
- **<2A><><EFBFBD>p<EFBFBD>覡**<2A>GSelf-hosted<65>]<5D>۫ء^<5E>A<EFBFBD>P Gateway / Mongo / Redis <20>P<EFBFBD><50><EFBFBD>ҩΦP VPC
- **LDAP <20><><EFBFBD><EFBFBD>**<2A>GZITADEL <20><><EFBFBD>һݯઽ<DDAF>s<EFBFBD><73><EFBFBD>~ AD / OpenLDAP<41>]<5D>`<60><><EFBFBD>GVPN<50>B<EFBFBD>M<EFBFBD>u<EFBFBD>B<EFBFBD><42> DMZ <20><><EFBFBD>o<EFBFBD>^
- **Management API / JWKS**<2A>GGateway <20>z<EFBFBD>L<EFBFBD><4C><EFBFBD><EFBFBD> URL <20>s<EFBFBD><73><EFBFBD>A<EFBFBD><41><EFBFBD>g<EFBFBD><67><EFBFBD><EFBFBD>
- **<2A>]<5D>w**<2A>G`etc/gateway.yaml` <20><> `Zitadel.Issuer` / `MgmtURL` <20><><EFBFBD>V self-hosted <20><><EFBFBD>I
2026-05-19 17:04:26 +00:00
### 3.4 <20><><EFBFBD>U<EFBFBD><55><EFBFBD>|<7C>]<5D>w<EFBFBD>M<EFBFBD><4D><EFBFBD>G<EFBFBD><47><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Gateway <20><><EFBFBD>U API<50>^
2026-05-19 17:04:26 +00:00
Gateway **<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>S** `/auth/register`<EFBFBD>C<EFBFBD><EFBFBD><EFBFBD>U<EFBFBD>ѤU<EFBFBD>C<EFBFBD><EFBFBD><EFBFBD>|<7C><><EFBFBD><EFBFBD><EFBFBD>G
2026-05-19 17:04:26 +00:00
| <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> | <20><><EFBFBD>U<EFBFBD><55><EFBFBD>| | <20><><EFBFBD><EFBFBD><EFBFBD>n<EFBFBD>J<EFBFBD>Ƨ@<40><> |
2026-05-19 17:04:26 +00:00
|---------|----------|----------------|
| **B2C** | ZITADEL Hosted Register UI<55>]<5D>Ϋe<CEAB>ݨ<EFBFBD> ZITADEL OIDC PKCE<43>^ | token exchange IJ<>o `EnsureFromOIDC` JIT |
| **B2B<32>]LDAP<41>^** | <20><> IT <20>b AD / OpenLDAP <20>رb<D8B1>F<EFBFBD>i<EFBFBD><69> Directory Sync <20>w provision <20><> ZITADEL | LDAP IdP <20>n<EFBFBD><4A>o `EnsureFromLDAP` JIT |
| **B2B<32>]SCIM<49>^** | HR / Okta / Entra <20><> SCIM Create User | SCIM endpoint <20>g ZITADEL + Gateway<61>]<5D><><EFBFBD><EFBFBD> JIT<49>^ |
2026-05-19 17:04:26 +00:00
> ZITADEL <20><><EFBFBD><EFBFBD> email <20><><EFBFBD>Ҥw<D2A4><77><EFBFBD><EFBFBD><EFBFBD>u**<2A>i<EFBFBD>n<EFBFBD>J**<2A>v<EFBFBD><76><EFBFBD>e<EFBFBD>F<EFBFBD>~<7E>ȤW<C8A4>u**<2A>i<EFBFBD>ϥΥ\<5C><>**<2A>v<EFBFBD><76><EFBFBD>e<EFBFBD><65> <20><>5.4 <20>~<7E><><EFBFBD><EFBFBD><EFBFBD>ҡC
2026-05-19 17:04:26 +00:00
### 3.5 <20><><EFBFBD>x MFA <20>j<EFBFBD><6A><EFBFBD>]<5D>w<EFBFBD>M<EFBFBD><4D><EFBFBD>^
2026-05-19 17:04:26 +00:00
- ZITADEL Org Policy <20>]<5D>w<EFBFBD>G**<2A><><EFBFBD><EFBFBD> admin <20><> role**<2A>]`tenant_owner` / `tenant_admin` / `platform_super_admin`<EFBFBD>^<5E>n<EFBFBD>J<EFBFBD>ɱj<C9B1><6A> TOTP / WebAuthn
- <20>@<40><> user <20>w<EFBFBD>]<5D><><EFBFBD>j<EFBFBD><6A><EFBFBD>]<5D>קK B2C <20>y<EFBFBD><79><EFBFBD>^
- <20><><EFBFBD><EFBFBD><EFBFBD>I<EFBFBD>~<7E>Ⱦާ@ <20><> <20><> Gateway Step-up MFA<46>]<5D><>5.6<EFBFBD>^<5E>A<EFBFBD>P ZITADEL <20><><EFBFBD><EFBFBD> MFA **<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>N**
2026-05-19 13:56:59 +00:00
---
## 4. auth <20>Ҳ<EFBFBD>
2026-05-19 13:56:59 +00:00
<EFBFBD><EFBFBD><EFBFBD>|<7C>G`internal/model/auth/`
2026-05-19 13:56:59 +00:00
### 4.1 ¾<>d
2026-05-19 13:56:59 +00:00
- <20><><EFBFBD><EFBFBD> ZITADEL OIDC token<65>]id_token / authorization_code + PKCE<43>^
- <20>s<EFBFBD><73> `member.EnsureFromOIDC` / `EnsureFromLDAP` / `EnsureFromSCIM` <20>P `permission.SyncRolesFromClaims`
- ñ<>o CloudEP JWT<57>]access + refresh<73>^
- **ñ<>o Step-up Token**<2A>]<5D><><EFBFBD><EFBFBD><EFBFBD>I<EFBFBD>ާ@<40>ΡA<CEA1>u<EFBFBD>ةR 5min<69>F<EFBFBD><46> <20><>5.6<EFBFBD>^
- <20>n<EFBFBD>X<EFBFBD>Gjti <20>¦W<C2A6><57>
- <20><><EFBFBD>q<EFBFBD><71><EFBFBD>ġG`auth_gen`<60>]<5D><><EFBFBD>v / <20><><EFBFBD>K<EFBFBD>X / <20>v<EFBFBD><76><EFBFBD>j<EFBFBD><6A><EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>^
2026-05-19 13:56:59 +00:00
### 4.2 UseCase <20><><EFBFBD><EFBFBD>
2026-05-19 13:56:59 +00:00
```go
type TokenUseCase interface {
Exchange(ctx context.Context, req *ExchangeRequest) (*TokenPair, error)
Refresh(ctx context.Context, req *RefreshRequest) (*TokenPair, error)
Logout(ctx context.Context, req *LogoutRequest) error
RevokeAllForUser(ctx context.Context, tenantID, uid string) error
}
2026-05-19 17:04:26 +00:00
type StepUpTokenUseCase interface {
Issue(ctx context.Context, tenantID, uid, action string) (stepUpToken string, err error)
Verify(ctx context.Context, token, expectedAction, tenantID, uid string) (jti string, err error)
MarkUsed(ctx context.Context, jti string) error // <20><EFBFBD><E6A6B8>
2026-05-19 17:04:26 +00:00
}
2026-05-19 13:56:59 +00:00
```
### 4.3 CloudEP JWT Claims
```go
type Claims struct {
jwt.RegisteredClaims // <20>t jti, exp, iat
2026-05-19 13:56:59 +00:00
TenantID string `json:"tenant_id"`
UID string `json:"uid"`
2026-05-19 17:04:26 +00:00
Typ string `json:"typ"` // access | refresh | step_up
AuthGen int64 `json:"auth_gen"` // <20><><EFBFBD>q<EFBFBD><71><EFBFBD>ĥN<C4A5><4E><EFBFBD><>o<EFBFBD><6F> = redis.GET <20><><EFBFBD>e<EFBFBD>ȡF<C8A1><46><EFBFBD>s<EFBFBD>b<EFBFBD><62><EFBFBD><EFBFBD> 0<>^
Action string `json:"action,omitempty"` // typ=step_up <20>ɥ<EFBFBD><C9A5><EFBFBD><EFBFBD>A<EFBFBD><41><EFBFBD>w<EFBFBD><77><EFBFBD>\<5C><><EFBFBD><EFBFBD><E6AABA><EFBFBD><EFBFBD><EFBFBD>I action
2026-05-19 13:56:59 +00:00
}
```
> **JWT <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> role / permission <20>ַ<EFBFBD>**<2A>CMiddleware <20>C<EFBFBD><43><EFBFBD>q `perm:user_roles:{tenant_id}:{uid}` cache Ū<><C5AA><EFBFBD><EFBFBD><EFBFBD>e role keys <20>A enforce<63>F<EFBFBD>קK<D7A7>u<EFBFBD><75><EFBFBD>W / <20>M<EFBFBD><4D> / <20>ܧ<EFBFBD><DCA7>v<EFBFBD><76><EFBFBD>v<EFBFBD><76><EFBFBD><EFBFBD> token <20>ٯ<EFBFBD><D9AF>ΡC
> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ܧ<EFBFBD><EFBFBD>ߧY<EFBFBD>ͮľa `auth_gen` + cache invalidate<74>F<EFBFBD><46><EFBFBD>̿<EFBFBD> token <20><><EFBFBD>e<EFBFBD>C
2026-05-19 17:04:26 +00:00
### 4.4 JWT <20>]<5D>w<EFBFBD>]go-zero<72>^+ Secret Rotation<6F>]<5D>w<EFBFBD>M<EFBFBD><4D><EFBFBD>^
2026-05-19 13:56:59 +00:00
```yaml
Auth:
AccessExpire: 900 # 15 <20><><EFBFBD><EFBFBD>
ActiveKID: v2 # <20><><EFBFBD><65>o<EFBFBD><6F> kid
Keys: # <20><><EFBFBD>ҥi<D2A5><69><EFBFBD><EFBFBD><EFBFBD><EFBFBD> kid <20>W<EFBFBD><57><EFBFBD>]<5D>t<EFBFBD><74><EFBFBD>b<EFBFBD>h<EFBFBD>Ъ<EFBFBD><D0AA>^
2026-05-19 17:04:26 +00:00
- kid: v1
Secret: ${JWT_ACCESS_SECRET_V1}
- kid: v2
Secret: ${JWT_ACCESS_SECRET_V2}
2026-05-19 13:56:59 +00:00
RefreshAuth:
AccessExpire: 604800 # 7 <20><>
2026-05-19 17:04:26 +00:00
ActiveKID: v2
Keys:
- kid: v1
Secret: ${JWT_REFRESH_SECRET_V1}
- kid: v2
Secret: ${JWT_REFRESH_SECRET_V2}
StepUp:
TokenTTLSeconds: 300
ActiveKID: v1
Keys:
- kid: v1
Secret: ${JWT_STEPUP_SECRET_V1}
2026-05-19 13:56:59 +00:00
```
**Rotation <20>y<EFBFBD>{<7B>G**
2026-05-19 17:04:26 +00:00
```
1. <20>s<EFBFBD>W v(N+1) key <20><> Keys<79>]<5D><><EFBFBD><EFBFBD> ActiveKID<49>^<5E><> rolling deploy
2. <20><> ActiveKID = v(N+1) <20><> <20>s token <20>ηs kid ñ<>F<EFBFBD><46> kid token <20><><EFBFBD>i<EFBFBD><69>
3. <20><><EFBFBD><EFBFBD> token <20><><EFBFBD><EFBFBD><EFBFBD>L<EFBFBD><4C><EFBFBD>]access 15min / refresh 7d<37>^
4. <20>q Keys <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> kid <20><> rolling deploy
2026-05-19 17:04:26 +00:00
```
- JWT header <20><><EFBFBD>a `kid`<EFBFBD>A<EFBFBD><EFBFBD><EFBFBD>Үɨ<EFBFBD> `kid` <20><> secret<65>F<EFBFBD><EFBFBD><E4A4A3> <20><> `401 invalid_kid`
- go-zero <20><><EFBFBD><EFBFBD> JWT middleware <20>ȦY<C8A6><59> secret<65>A**<2A>ۼg `JwtMultiKeyMiddleware`** <20><><EFBFBD>N<EFBFBD>Ϋe<CEAB>m<EFBFBD>]<5D>b `JwtRevokeMiddleware` <20><><EFBFBD>e<EFBFBD>^
- ZITADEL Token Exchange<67>BStep-up <20>@<40>Φ<EFBFBD><CEA6>[<5B>c
2026-05-19 17:04:26 +00:00
`.api` <20><><EFBFBD>O<EFBFBD>@<40><><EFBFBD>ѡG
2026-05-19 13:56:59 +00:00
```api
2026-05-19 17:04:26 +00:00
@server(jwt: Auth, middleware: JwtMultiKeyMiddleware,JwtRevokeMiddleware)
2026-05-19 13:56:59 +00:00
```
### 4.5 <20>¦W<C2A6><EFBFBD><E6B5A6><EFBFBD>]<5D>u<EFBFBD>¦W<C2A6><57> JWT<57>^
2026-05-19 13:56:59 +00:00
#### Issue Token Pair <20>ɰO<C9B0><4F><EFBFBD><EFBFBD><EFBFBD>]<5D><> logout <20><><EFBFBD><EFBFBD><EFBFBD>a refresh<73>^
2026-05-19 17:04:26 +00:00
```
SET auth:jwt:pair:{access_jti} = refresh_jti TTL = access TTL
SET auth:jwt:pair:{refresh_jti} = access_jti TTL = refresh TTL
```
#### <20><> Token <20>M<EFBFBD>P<EFBFBD>]<5D>n<EFBFBD>X<EFBFBD>^
2026-05-19 13:56:59 +00:00
```
Key: auth:jwt:bl:{jti}
Value: 1
TTL: token <20>Ѿl<D1BE><6C><EFBFBD>Įɶ<C4AE><C9B6>]exp - now<6F>^
2026-05-19 13:56:59 +00:00
```
2026-05-19 17:04:26 +00:00
```
POST /auth/logout (Bearer access_jwt)
1. <20><> access_jti <20><> SET auth:jwt:bl:{access_jti}
2. GET auth:jwt:pair:{access_jti} <20><> refresh_jti<74>]<5D>Y<EFBFBD>s<EFBFBD>b<EFBFBD>^
2026-05-19 17:04:26 +00:00
3. SET auth:jwt:bl:{refresh_jti}
4. DEL auth:jwt:pair:{access_jti} / auth:jwt:pair:{refresh_jti}
```
#### Refresh Token <20><><EFBFBD><EFBFBD><EFBFBD>]<5D>w<EFBFBD>M<EFBFBD><4D><EFBFBD>^+ Reuse Detection
2026-05-19 17:04:26 +00:00
```
POST /auth/token/refresh
1. <20><><EFBFBD><EFBFBD> refresh_jwt<77>]typ=refresh<73>B<EFBFBD><42><EFBFBD>L<EFBFBD><4C><EFBFBD>Bauth_gen <20><><EFBFBD>ġ^
2. <20>Y refresh_jti <20>w<EFBFBD>b<EFBFBD>¦W<C2A6><57><EFBFBD>G
<20><><EFBFBD><EFBFBD><EFBFBD>Q<EFBFBD>ѩέ<D1A9><CEAD><EFBFBD> <20><> INCR auth:gen:{tenant_id}:{uid}<7D>]<5D>M<EFBFBD>P<EFBFBD><50><EFBFBD><EFBFBD> chain<69>^
<20>^ 401<30>A<EFBFBD>üg audit log
3. ñ<>o<EFBFBD>s access_jwt + <20>s refresh_jwt<77>]<5D>s jti<74>^
4. <20><><EFBFBD><EFBFBD> refresh_jti<74>F<EFBFBD>Y<EFBFBD><59> access <20><><EFBFBD><EFBFBD> jti <20><><EFBFBD><EFBFBD><EFBFBD>L<EFBFBD><4C><EFBFBD>A<EFBFBD>@<40>ֶ¦W<C2A6><57>
5. <20>g<EFBFBD>J<EFBFBD>s<EFBFBD><73> auth:jwt:pair
2026-05-19 17:04:26 +00:00
```
- <20>C<EFBFBD><43> refresh <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]Refresh Token Rotation<6F>^
- **Reuse detection**<2A>G<EFBFBD><47> refresh <20>Q<EFBFBD>ĤG<C4A4><47><EFBFBD>ϥ<EFBFBD> <20><> <20><><EFBFBD>P<EFBFBD>s<EFBFBD>ΡA<CEA1>ߧY<DFA7><59><EFBFBD>q<EFBFBD>M<EFBFBD>P<EFBFBD><50> user
2026-05-19 13:56:59 +00:00
#### Token Exchange <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
2026-05-19 17:04:26 +00:00
```
POST /auth/token/exchange { tenant_slug, id_token }
1. zitadel.VerifyIDToken<65>]<5D><> aud<75>Biss<73>Bexp<78>Bsignature<72>^
2. <20>j<EFBFBD><6A><EFBFBD>ˬd id_token.iat <20>b<EFBFBD>̪<EFBFBD> 5 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
3. SETNX auth:exchange:nonce:{id_token.jti}=1 TTL 10min<69>F<EFBFBD><46><EFBFBD><EFBFBD> <20><> 409 <20>w<EFBFBD>ϥ<EFBFBD>
4. <20><><EFBFBD><EFBFBD> tenant_slug <20><> tenant.org_id == id_token.org_id
2026-05-19 17:04:26 +00:00
5. EnsureFromOIDC / SyncRoles / IssueTokenPair
```
#### Step-up Token<65>]<5D><EFBFBD>ʡB<CAA1><42> action<6F>^
2026-05-19 17:04:26 +00:00
```
Key: auth:stepup:used:{jti} SETNX TTL = step_up_token TTL
Value: 1
```
- TTL<54>G5 <20><><EFBFBD><EFBFBD>
- Claims<6D>G`typ=step_up` + `action`<EFBFBD>]<5D>p `change_business_email`<EFBFBD>^
- Logic <20>h<EFBFBD>u<EFBFBD><75><EFBFBD>G
1. <20><> step_up JWT <20><> <20><> `typ == "step_up"`<EFBFBD>B`tenant_id`<60>B`uid`<60>B`action == expected`
2. `SETNX auth:stepup:used:{jti}=1`<EFBFBD>A<EFBFBD>w<EFBFBD>s<EFBFBD>b <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>A<EFBFBD>ڵ<EFBFBD>
3. <20>q<EFBFBD>L<EFBFBD><4C><EFBFBD><EFBFBD><EFBFBD><EFBFBD><E6B0AA><EFBFBD>I<EFBFBD>ާ@<40>Ftoken <20>Y<EFBFBD>@<40>o
- Step-up token **<EFBFBD><EFBFBD>**<2A>i jti <20>¦W<C2A6><57><EFBFBD>t<EFBFBD>ΡF<CEA1><EFBFBD>ʾa `auth:stepup:used` <20>Y<EFBFBD>i
2026-05-19 17:04:26 +00:00
#### <20><><EFBFBD>q<EFBFBD><71><EFBFBD>ġ]<5D><><EFBFBD>v / <20><><EFBFBD>K<EFBFBD>X / SCIM deactivate / **<2A>v<EFBFBD><76><EFBFBD>ܧ<EFBFBD>**<2A>^
2026-05-19 13:56:59 +00:00
```
Key: auth:gen:{tenant_id}:{uid}
Value: <20><><EFBFBD>ơA<C6A1>w<EFBFBD>] 1<>F<EFBFBD>ƥ<EFBFBD><C6A5>o<EFBFBD>ͮ<EFBFBD> INCR
2026-05-19 13:56:59 +00:00
```
Middleware <20>ˬd<CBAC>G`token.auth_gen >= redis.auth_gen`<60>A<EFBFBD>_<EFBFBD>h 401<30>C
2026-05-19 13:56:59 +00:00
> **<2A>w<EFBFBD>M<EFBFBD><4D>**<2A>GUserRole <20><><EFBFBD><EFBFBD>/<2F>M<EFBFBD>P<EFBFBD>B<EFBFBD>~<7E><> Group <20>M<EFBFBD>g<EFBFBD>ɭP<C9AD><50> user role <20>ܧ<EFBFBD> <20><> **`INCR auth_gen`**<2A>]<5D><><EFBFBD>ıj<C4B1><6A><EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>A<EFBFBD>ϥΪ̻ݭ<CCBB><DDAD>s exchange/refresh <20><><EFBFBD>o<EFBFBD>s auth_gen<65>^<5E>C
2026-05-19 17:04:26 +00:00
>
> RolePermission <20>ܧ󤣧<DCA7><F3A4A3A7>ܡu<DCA1>ϥΪ̦<CEAA><CCA6><EFBFBD><EFBFBD>Ǩ<EFBFBD><C7A8><EFBFBD><EFBFBD>v<EFBFBD>A<EFBFBD>u<EFBFBD><75> `LoadPolicy(tenant_id)` + <20>v<EFBFBD><76><EFBFBD>֨<EFBFBD><D6A8><EFBFBD><EFBFBD>ġF<C4A1>Y<EFBFBD><59><EFBFBD>ӧ令<D3A7><EFA6A8><EFBFBD><EFBFBD><EFBFBD>H<EFBFBD><48> JWT <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>/<2F>v<EFBFBD><76><EFBFBD>ַӡA<D3A1>~<7E>ݭn<DDAD>P<EFBFBD>B `INCR auth_gen`<60>C
2026-05-19 17:04:26 +00:00
> JWT <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> permission<6F>]<5D>קK token <20>L<EFBFBD>j<EFBFBD>^<5E>F<EFBFBD><46><EFBFBD>q<EFBFBD><71><EFBFBD>ĥ<EFBFBD> `auth_gen`<60>A<EFBFBD><EFBFBD>n<EFBFBD>X<EFBFBD><58> jti <20>¦W<C2A6><57><EFBFBD>C
2026-05-19 13:56:59 +00:00
### 4.6 Middleware <20>ˬd<CBAC><64><EFBFBD><EFBFBD>
2026-05-19 13:56:59 +00:00
```
0. Platform Admin allowlist <20>R<EFBFBD><52><EFBFBD>]platform tenant + platform_super_admin role <20><> break-glass UID<49>^
<20><> audit.LogPlatformBypass <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
1. go-zero JWT <20><>ñ + exp
2. typ == "access"<22>]<5D><><EFBFBD>O<EFBFBD>@ API<50>^
2026-05-19 13:56:59 +00:00
3. NOT EXISTS auth:jwt:bl:{jti}
4. claims.auth_gen >= redis auth:gen:{tenant}:{uid}
- redis key <20><><EFBFBD>s<EFBFBD>b <20><> <20><><EFBFBD><EFBFBD> 0
- ñ<>o token <20><> claims.auth_gen = redis.GET <20><> 0
5. <20>`<60>J context<78>Gtenant_id, uid<69>]role keys <20>ѤU<D1A4>@<40>h CasbinRBACMiddleware <20>q cache <20><><EFBFBD>J<EFBFBD>^
2026-05-19 13:56:59 +00:00
```
---
## 5. member <20>Ҳ<EFBFBD>
2026-05-19 13:56:59 +00:00
<EFBFBD><EFBFBD><EFBFBD>|<7C>G`internal/model/member/`
2026-05-19 13:56:59 +00:00
### 5.1 ¾<>d
2026-05-19 13:56:59 +00:00
- <20>|<7C><> Profile CRUD<55>]tenant-scoped<65>^
- Identity <20>M<EFBFBD>g<EFBFBD>]`zitadel_sub` ? `uid`<EFBFBD>^
- Tenant metadata <20>P LDAP <20>P<EFBFBD>B<EFBFBD>]<5D>w
- UID <20><><EFBFBD>͡]<5D><69><EFBFBD>^
- SCIM <20>~<7E>ȼg<C8BC>J<EFBFBD>]SCIM `id` / Gateway UID + <20>Ȥ<EFBFBD><C8A4><EFBFBD> `externalId`<EFBFBD>^
- Directory Sync Worker<65>]AD + OpenLDAP<41>^
- <20>|<7C><><EFBFBD><EFBFBD><EFBFBD>A<EFBFBD>]active / suspended / deleted<65>^<5E><> <20>q<EFBFBD><71> auth <20>M<EFBFBD>P token
- **<2A>~<7E>ȯ<EFBFBD><C8AF><EFBFBD><EFBFBD><EFBFBD>**<2A>Gbusiness email / phone <20>j<EFBFBD>w + OTP <20>۰e
- **Step-up MFA OTP <20><><EFBFBD><EFBFBD>**<2A>]<5D>f<EFBFBD>t auth <20>Ҳ<EFBFBD>ñ step_up_token<65>^
2026-05-19 13:56:59 +00:00
### 5.2 UseCase <20><><EFBFBD><EFBFBD>
2026-05-19 13:56:59 +00:00
> **<2A>]<5D>p<EFBFBD><70><EFBFBD>h<EFBFBD>]<5D>I<EFBFBD><49> model.md<6D>^**<2A>G<EFBFBD>C<EFBFBD><43> UseCase <20>O**<2A><><EFBFBD>l<EFBFBD>~<7E>Ⱦާ@**<2A>A**<2A><><EFBFBD><EFBFBD><EFBFBD>]<5D>e<EFBFBD><65><EFBFBD>B<EFBFBD>J<EFBFBD>s<EFBFBD>b**<2A>C<EFBFBD>y<EFBFBD>{<7B>s<EFBFBD>ơ]<5D>p<EFBFBD>u<EFBFBD><75><EFBFBD>U <20><> <20>H<EFBFBD><48><EFBFBD>ҫH <20><> <20>ҥΡv<CEA1>^<5E><> **logic <20>h**<2A>Φh<CEA6><68> UseCase <20><><EFBFBD>ˡF<CBA1><46><EFBFBD>h<EFBFBD>u<EFBFBD>t<EFBFBD>d<EFBFBD><64><EFBFBD>@<40>ʧ@ + <20>Ƨ@<40>ΡC
2026-05-19 17:04:26 +00:00
>
> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>h<EFBFBD>G
> 1. **Atomic primitives**<2A>G<EFBFBD>º骺<C2BA><E9AABA><EFBFBD>@<40>ʧ@<40>]<5D><> member<65>B<EFBFBD><42> OTP<54>B<EFBFBD><42> OTP<54>B<EFBFBD>H notification<6F>^<5E>CLogic <20>i<EFBFBD><69><EFBFBD>N<EFBFBD>զX<D5A6>A<EFBFBD><41><EFBFBD>y<EFBFBD>{<7B>@<40>ΡC
> 2. **Composite**<2A>G<EFBFBD><47><EFBFBD>X<EFBFBD>ӱ`<60><> atomic <20>w<EFBFBD><77><EFBFBD>զn<D5A6><6E><EFBFBD>u<EFBFBD>ֱ<EFBFBD><D6B1>զX<D5A6>v<EFBFBD>]<5D>p `VerificationUseCase` = `OTP.Generate` + `Notifier.Send` + `Member.SetVerified`<60>^<5E>CComposite <20>O**<2A>i<EFBFBD><69>**<2A>Alogic <20>]<5D>i<EFBFBD><48>L<EFBFBD><4C><EFBFBD><EFBFBD><EFBFBD><EFBFBD> atomic<69>C
2026-05-19 17:04:26 +00:00
>
> <EFBFBD>~<7E><><EFBFBD>޿<EFBFBD><DEBF>]API<50>Bhandler<65>B<EFBFBD>y<EFBFBD>{<7B>s<EFBFBD>ơ^<5E>ثe**<2A><><EFBFBD><EFBFBD><EFBFBD>@**<2A>F<EFBFBD><46><EFBFBD>T<EFBFBD>Ƥ<EFBFBD><C6A4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>C
2026-05-19 17:04:26 +00:00
#### 5.2.1 Atomic primitives
2026-05-19 13:56:59 +00:00
2026-05-19 17:04:26 +00:00
```go
// <20>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w
// Profile<6C><47>g member <20><><EFBFBD><EFBFBD><EFBFBD>]<5D><><EFBFBD>t<EFBFBD>ҥ<EFBFBD> / <20><><EFBFBD>v<EFBFBD><76><EFBFBD><EFBFBD><EFBFBD>A<EFBFBD>ܾE<DCBE>^
// <20>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w
2026-05-19 13:56:59 +00:00
type ProfileUseCase interface {
GetByUID(ctx context.Context, req *GetMemberRequest) (*MemberDTO, error)
Update(ctx context.Context, req *UpdateMemberRequest) (*MemberDTO, error)
List(ctx context.Context, req *ListMembersRequest) (*ListMembersResponse, error)
2026-05-19 17:04:26 +00:00
// <20>~<7E><> email / phone <20>X<EFBFBD>Ф<EFBFBD><D0A4><EFBFBD><EFBFBD>]<5D>Q Verification <20>Υ~<7E><><EFBFBD>y<EFBFBD>{<7B>ϥΡ^
2026-05-19 17:04:26 +00:00
SetBusinessEmailVerified(ctx context.Context, tenantID, uid, email string) error
SetBusinessPhoneVerified(ctx context.Context, tenantID, uid, phone string) error
}
// <20>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w
// Lifecycle<6C>G<EFBFBD><47><EFBFBD>A<EFBFBD>ܾE<DCBE><45><EFBFBD><EFBFBD><EFBFBD>@<40>ʧ@<40>F<EFBFBD><46><EFBFBD>H<EFBFBD>H<EFBFBD>B<EFBFBD><42>ñ token
// <20>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w
2026-05-19 17:04:26 +00:00
type LifecycleUseCase interface {
// <20><><EFBFBD>x<EFBFBD><78><EFBFBD>͵<EFBFBD><CDB5>U<EFBFBD>G<EFBFBD>إ<EFBFBD> unverified member<65>]<5D><><EFBFBD>H OTP<54>A<EFBFBD><41><EFBFBD>o token<65>^
2026-05-19 17:04:26 +00:00
CreateUnverified(ctx context.Context, req *CreatePlatformMemberRequest) (*MemberDTO, error)
// <20>ҥΡGunverified <20><> active<76>Fcaller <20><><EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>O<EFBFBD>Ҧ<EFBFBD><D2A6>e<EFBFBD>m<EFBFBD><6D><EFBFBD>Ҥw<D2A4>q<EFBFBD>L
2026-05-19 17:04:26 +00:00
Activate(ctx context.Context, tenantID, uid string) error
// <20><><EFBFBD>v<EFBFBD>Gactive <20><> suspended<65>F<EFBFBD><46><EFBFBD>M token<65>]<5D>M token <20><> auth <20>Ҳհ<D2B2><D5B0>^
2026-05-19 17:04:26 +00:00
Suspend(ctx context.Context, tenantID, uid, reason string) error
// <20>_<EFBFBD>v<EFBFBD>Gsuspended <20><> active
2026-05-19 17:04:26 +00:00
Reactivate(ctx context.Context, tenantID, uid string) error
// <20>n<EFBFBD>R<EFBFBD>Gactive|suspended <20><> deleted<65>]<5D><><EFBFBD>|<7C>ߨ<EFBFBD><DFA8>ΦW<CEA6>ơF30 <20>ѫ<EFBFBD><D1AB><EFBFBD> worker <20>B<EFBFBD>z <20><>5.7<EFBFBD>^
2026-05-19 17:04:26 +00:00
SoftDelete(ctx context.Context, tenantID, uid string) error
// <20><><EFBFBD><EFBFBD>ҥε<D2A5><CEB5>U<EFBFBD>]<5D>O<EFBFBD>ɲM<C9B2>z<EFBFBD>F<EFBFBD>u<EFBFBD><75><EFBFBD><EFBFBD> unverified <20>Ρ^
2026-05-19 17:04:26 +00:00
AbortPending(ctx context.Context, tenantID, uid string) error
}
// <20>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w
// Provisioning<6E>G<EFBFBD>~<7E><><EFBFBD>ӷ<EFBFBD> <20><> Gateway member <20><> JIT / sync upsert
// <20>C<EFBFBD>Өӷ<D3A8><D3B7>W<EFBFBD>ߤ@<40>Ӱʧ@<40>Femail <20><><EFBFBD><EFBFBD><EFBFBD>ӷ<EFBFBD> IdP <20>w<EFBFBD><77><EFBFBD>ҡA<D2A1><41><EFBFBD>A<EFBFBD><41> OTP
// <20>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w
2026-05-19 17:04:26 +00:00
type ProvisioningUseCase interface {
// ZITADEL OIDC token exchange<67>G<EFBFBD><47> id_token claims <20>W upsert<72>]B2C / Social IdP<64>^
2026-05-19 17:04:26 +00:00
EnsureFromOIDC(ctx context.Context, req *EnsureFromOIDCRequest) (*MemberDTO, error)
// ZITADEL LDAP IdP <20>n<EFBFBD>J<EFBFBD><4A> JIT<49>F<EFBFBD><46> Directory Sync worker <20><><EFBFBD>e
2026-05-19 17:04:26 +00:00
EnsureFromLDAP(ctx context.Context, req *EnsureFromLDAPRequest) (*MemberDTO, error)
// SCIM Create / Update User
EnsureFromSCIM(ctx context.Context, req *EnsureFromSCIMRequest) (*MemberDTO, error)
2026-05-19 13:56:59 +00:00
}
// <20>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w
// OTP<54>Gatomic<69>Bpurpose-agnostic <20>@<40><><EFBFBD>ʱK<CAB1>X
// <20><><EFBFBD>H<EFBFBD>H<EFBFBD>B<EFBFBD><42><EFBFBD><EFBFBD><EFBFBD>s member<65>Fcaller <20><> code <20><><EFBFBD>ۦ<EFBFBD><DBA6>z<EFBFBD>L NotifierUseCase <20>
// <20>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w
2026-05-19 17:04:26 +00:00
type OTPUseCase interface {
// <20>ͦ<EFBFBD><CDA6>Gbcrypt <20>s redis<69>A<EFBFBD>^ challenge_id + <20><><EFBFBD>X code<64>]<5D>@<40><><EFBFBD>ʦ^<5E>ǡ^
2026-05-19 17:04:26 +00:00
Generate(ctx context.Context, req *GenerateOTPRequest) (*OTPChallengeDTO, error)
// <20><><EFBFBD>ҡG<D2A1><47><EFBFBD>\<5C>h invalidate<74>Fpurpose <20><><EFBFBD><EFBFBD><EFBFBD>P challenge <20>إ߮ɤ@<40>P
2026-05-19 17:04:26 +00:00
Verify(ctx context.Context, req *VerifyOTPRequest) error
// <20>D<EFBFBD>ʥ<EFBFBD><CAA5>ġ]<5D><> challenge / <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>U<EFBFBD>^
2026-05-19 17:04:26 +00:00
Invalidate(ctx context.Context, tenantID, challengeID string) error
2026-05-19 13:56:59 +00:00
}
// <20>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w
// TOTP<54>]Authenticator App<70>^<5E>G<EFBFBD><47> <20><>5.8
// <20>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w
2026-05-19 17:04:26 +00:00
type TOTPUseCase interface {
StartEnroll(ctx context.Context, tenantID, uid string) (*EnrollStartDTO, error)
ConfirmEnroll(ctx context.Context, tenantID, uid, code string) (backupCodes []string, err error)
VerifyCode(ctx context.Context, tenantID, uid, code string) error
Disable(ctx context.Context, tenantID, uid string) error
RegenerateBackupCodes(ctx context.Context, tenantID, uid string) ([]string, error)
}
// <20>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w
2026-05-19 17:04:26 +00:00
// Tenant
// <20>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w
2026-05-19 13:56:59 +00:00
type TenantUseCase interface {
Create(ctx context.Context, req *CreateTenantRequest) (*TenantDTO, error)
ResolveBySlug(ctx context.Context, slug string) (*TenantDTO, error)
ConfigureLDAP(ctx context.Context, req *ConfigureLDAPRequest) error
}
// <20>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w
2026-05-19 17:04:26 +00:00
// SCIM Resource handlers
// <20>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w<EFBFBD>w
2026-05-19 13:56:59 +00:00
type ScimUseCase interface {
CreateUser(ctx context.Context, req *ScimCreateUserRequest) (*ScimUserDTO, error)
GetUser(ctx context.Context, req *ScimGetUserRequest) (*ScimUserDTO, error)
PatchUser(ctx context.Context, req *ScimPatchUserRequest) (*ScimUserDTO, error)
DeleteUser(ctx context.Context, req *ScimDeleteUserRequest) error
PatchGroup(ctx context.Context, req *ScimPatchGroupRequest) error
}
type DirectorySyncUseCase interface {
SyncTenant(ctx context.Context, tenantID string) (*SyncResult, error)
}
```
#### 5.2.2 Composite<74>]<5D>i<EFBFBD><69><EFBFBD>F<EFBFBD>`<60>βզX<D5A6><58><EFBFBD>K<EFBFBD>Q<EFBFBD>]<5D>^
2026-05-19 17:04:26 +00:00
> Composite <20><><EFBFBD><EFBFBD><EFBFBD>u<EFBFBD>I<EFBFBD>s Atomic primitives + library / notifier<65>A**<2A><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>󤣥i<F3A4A3A5><69> atomic <20><><EFBFBD>X<EFBFBD><58><EFBFBD>Ƨ@<40><>**<2A>C
> Logic <20>i<EFBFBD><69><EFBFBD>ܥ<EFBFBD> composite<74><><EFBFBD>p<EFBFBD>^<5E>Ϊ<EFBFBD><CEAA><EFBFBD><EFBFBD><EFBFBD> atomic<69>]<5D>S<EFBFBD><53><EFBFBD>ݨD<DDA8>^<5E>C
2026-05-19 17:04:26 +00:00
```go
// <20>~<7E><> email / phone <20><><EFBFBD><EFBFBD> = OTP.Generate + Notifier.Send + Profile.SetXxxVerified
2026-05-19 17:04:26 +00:00
type VerificationUseCase interface {
StartEmailVerify(ctx context.Context, tenantID, uid, target string) (*OTPChallengeDTO, error)
ConfirmEmailVerify(ctx context.Context, tenantID, uid, challengeID, code string) error
StartPhoneVerify(ctx context.Context, tenantID, uid, target string) (*OTPChallengeDTO, error)
ConfirmPhoneVerify(ctx context.Context, tenantID, uid, challengeID, code string) error
}
// Step-up = (TOTP.VerifyCode <20><> OTP.Generate+Notifier.Send/OTP.Verify) + auth.StepUpToken.Issue
2026-05-19 17:04:26 +00:00
type StepUpUseCase interface {
Start(ctx context.Context, tenantID, uid string, req *StepUpStartRequest) (*StepUpChallengeDTO, error)
Confirm(ctx context.Context, tenantID, uid string, req *StepUpConfirmRequest) (stepUpToken string, err error)
}
```
#### 5.2.3 Request / DTO <20><><EFBFBD><EFBFBD>
2026-05-19 17:04:26 +00:00
```go
// Provisioning
type EnsureFromOIDCRequest struct {
TenantID string
ZitadelSub string
Email string
EmailVerified bool // <20>Ӧ<EFBFBD> id_token claim<69>FOIDC <20>q<EFBFBD>` true
2026-05-19 17:04:26 +00:00
DisplayName string
Locale string
RawClaims map[string]any
}
type EnsureFromLDAPRequest struct {
TenantID string
ExternalID string // objectGUID / entryUUID
LDAPDN string
Username string
Email string
DisplayName string
Groups []string
Source enum.RoleSource // ldap_sync | ldap_jit
}
type EnsureFromSCIMRequest struct {
TenantID string
ExternalID string // SCIM externalId<49>]<5D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> UID<49>^
2026-05-19 17:04:26 +00:00
UserName string
Email string
DisplayName string
Active bool
RawPayload map[string]any
}
// Platform registration
type CreatePlatformMemberRequest struct {
TenantID string
Email string
PasswordHash string // <20>Y<EFBFBD>ϥ<EFBFBD> ZITADEL local user<65>A<EFBFBD>d<EFBFBD>š]<5D><> ZITADEL <20>ޡ^
2026-05-19 17:04:26 +00:00
DisplayName string
Language string
// <20><><EFBFBD>|<7C>ߧY active<76>F<EFBFBD>s<EFBFBD><73> member.status = unverified
2026-05-19 17:04:26 +00:00
}
// OTP
type GenerateOTPRequest struct {
TenantID string
Purpose enum.OTPPurpose // registration_email | business_email | business_phone | step_up | password_reset | ...
Identifier string // <20>q<EFBFBD>`<60>O uid<69>F<EFBFBD><46><EFBFBD>U<EFBFBD><55> uid <20>|<7C><><EFBFBD>s<EFBFBD>b<EFBFBD>ɥi<C9A5><69> hash(email)
Length int // 0 = <20><> config <20>w<EFBFBD>]<5D>]6<>^
TTLSeconds int // 0 = <20><> config <20>w<EFBFBD>]<5D>]300<30>^
2026-05-19 17:04:26 +00:00
}
type OTPChallengeDTO struct {
ChallengeID string
Code string // <20><> Generate <20>ɦ^<5E>Ǥ@<40><><EFBFBD>]<5D><><EFBFBD>X<EFBFBD>^<5E>Fcaller <20>ۭt<DBAD>
2026-05-19 17:04:26 +00:00
ExpiresIn int
}
type VerifyOTPRequest struct {
TenantID string
ChallengeID string
Code string
Purpose enum.OTPPurpose // <20><><EFBFBD><EFBFBD><EFBFBD>A<EFBFBD><41> challenge <20>Q<EFBFBD>ɥΨ<C9A5><CEA8><EFBFBD><EFBFBD>L<EFBFBD>γ~
2026-05-19 17:04:26 +00:00
}
// Step-up
type StepUpStartRequest struct {
TenantID string
UID string
Action enum.StepUpAction
PreferChannel enum.Channel // <20>i<EFBFBD><69><EFBFBD>Gtotp | sms | email<69>F<EFBFBD><46><EFBFBD><EFBFBD><EFBFBD>w<EFBFBD>h<EFBFBD><68> <20><>5.6 <20>u<EFBFBD><75><EFBFBD><EFBFBD>
2026-05-19 17:04:26 +00:00
}
type StepUpChallengeDTO struct {
ChallengeID string // TOTP <20>L challenge_id <20>]<5D>i<EFBFBD>^<5E>T<EFBFBD>w<EFBFBD>ȡFConfirm <20>ɤ<EFBFBD><C9A4>|<7C>h<EFBFBD><68><EFBFBD><EFBFBD>
2026-05-19 17:04:26 +00:00
Channel enum.Channel
ExpiresIn int
}
type StepUpConfirmRequest struct {
TenantID string
UID string
ChallengeID string
Code string
Action enum.StepUpAction
}
```
#### 5.2.4 Enum <20><><EFBFBD><EFBFBD>
2026-05-19 17:04:26 +00:00
```go
// member/enum/otp_purpose.go
type OTPPurpose string
const (
OTPPurposeRegistrationEmail OTPPurpose = "registration_email"
OTPPurposeBusinessEmail OTPPurpose = "business_email"
OTPPurposeBusinessPhone OTPPurpose = "business_phone"
OTPPurposeStepUp OTPPurpose = "step_up"
OTPPurposePasswordReset OTPPurpose = "password_reset" // <20>w<EFBFBD>d
2026-05-19 17:04:26 +00:00
)
// auth/enum/step_up_action.go<67>]<5D>w<EFBFBD>s<EFBFBD>b<EFBFBD><62> <20><>5.6<EFBFBD>A<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ŧi<EFBFBD>󦹡^
2026-05-19 17:04:26 +00:00
type StepUpAction string
const (
StepUpChangeBusinessEmail StepUpAction = "change_business_email"
StepUpChangeBusinessPhone StepUpAction = "change_business_phone"
StepUpDeleteMember StepUpAction = "delete_member"
StepUpTenantAdminForceStatus StepUpAction = "tenant_admin_force_status"
StepUpRevokeAllSessions StepUpAction = "revoke_all_sessions"
StepUpDisableTOTP StepUpAction = "disable_totp"
)
```
### 5.3 <20>|<7C><><EFBFBD>ͩR<CDA9>g<EFBFBD><67><EFBFBD><EFBFBD><EFBFBD>A
2026-05-19 13:56:59 +00:00
| <20><><EFBFBD>A | <20>y<EFBFBD>N | <20>Ƨ@<40><> |
2026-05-19 13:56:59 +00:00
|------|------|--------|
| `unverified` | **<EFBFBD>ȥ<EFBFBD><EFBFBD>x<EFBFBD><EFBFBD><EFBFBD>͵<EFBFBD><EFBFBD>U**<2A>|<7C>X<EFBFBD>{<7B>Gmember <20>w<EFBFBD>إߡA<DFA1><41><EFBFBD><EFBFBD><EFBFBD>U email <20>|<7C><><EFBFBD>q<EFBFBD>L OTP <20><><EFBFBD><EFBFBD> | <20><>ñ token<65>B<EFBFBD><42><EFBFBD>i<EFBFBD>n<EFBFBD>J<EFBFBD>F<EFBFBD>O<EFBFBD><4F><EFBFBD><EFBFBD> cron `AbortPending` <20>M<EFBFBD>z |
| `active` | <20><><EFBFBD>`<60>ϥ<EFBFBD> | <20>X |
| `suspended` | <20><><EFBFBD>v<EFBFBD>]<5D>޲z<DEB2><7A><EFBFBD>ާ@ / <20><><EFBFBD><EFBFBD><EFBFBD>^ | `auth.RevokeAllForUser`<EFBFBD>]`INCR auth_gen`<60>^ |
| `deleted` | <20>n<EFBFBD>R<EFBFBD><52> | <20>M cache<68>B<EFBFBD>M<EFBFBD>P token<65>BZITADEL disable<6C>F30 <20>ѫ<EFBFBD><D1AB>ΦW<CEA6>ơ]<5D><>5.7<EFBFBD>^ |
2026-05-19 17:04:26 +00:00
> <EFBFBD>Ӧ<EFBFBD> OIDC / LDAP / SCIM <20><> member **<2A><><EFBFBD><EFBFBD><EFBFBD>ج<EFBFBD> `active`**<2A>]email <20>Ѩӷ<D1A8> IdP <20>w<EFBFBD><77><EFBFBD>ҡ^<5E>F<EFBFBD>u<EFBFBD><75> platform-native <20><><EFBFBD>U<EFBFBD>|<7C>g<EFBFBD>L `unverified`<60>C
> <EFBFBD>~<7E><> email / phone <20><><EFBFBD>ҥH<D2A5>W<EFBFBD>ߺX<DFBA>С]`BusinessEmailVerified` / `BusinessPhoneVerified`<60>^<5E><><EFBFBD>ܡA<DCA1>P<EFBFBD>ͩR<CDA9>g<EFBFBD><67><EFBFBD><EFBFBD><EFBFBD>A<EFBFBD>ѽ<EFBFBD><D1BD>C
2026-05-19 17:04:26 +00:00
#### Member <20><><EFBFBD><EFBFBD> Source of Truth<74>]<5D>w<EFBFBD>M<EFBFBD><4D><EFBFBD>^
2026-05-19 17:04:26 +00:00
| <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>O | <20>d<EFBFBD><64> | SoT | <20>欰 |
2026-05-19 17:04:26 +00:00
|---------|------|-----|------|
| <20><><EFBFBD><EFBFBD><EFBFBD>ѧO | `zitadel_sub`<EFBFBD>B`ZitadelEmail`<60>B`DisplayName`<60>]IdP<64>^<5E>BZITADEL `status` | **ZITADEL** | <20>C<EFBFBD><43> token exchange / webhook <20>P<EFBFBD>B<EFBFBD>FGateway <20><><EFBFBD>i<EFBFBD><69><EFBFBD>g |
| <20>~<7E>ȸ<EFBFBD><C8B8><EFBFBD> | `BusinessEmail/Phone(+Verified)`<EFBFBD>B`Language`<60>B`Currency`<60>B`Avatar`<60>B`Preferences` | **Gateway** | <20>~<7E><> API <20>g<EFBFBD>F<EFBFBD><46><EFBFBD>^<5E><> ZITADEL |
| Provisioning <20>ӷ<EFBFBD> | `external_id`<EFBFBD>B`ldap_dn`<60>BSCIM <20>s<EFBFBD>զ<EFBFBD><D5A6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> | **<EFBFBD>ӷ<EFBFBD><EFBFBD>t<EFBFBD><EFBFBD>**<2A>]LDAP/SCIM<49>^ | sync replace<63>FGateway <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>s<EFBFBD><73> |
2026-05-19 17:04:26 +00:00
> <EFBFBD><EFBFBD><EFBFBD>סG`Member.Origin` <20>ХD<D0A5>ӷ<EFBFBD><D3B7>F<EFBFBD><46><EFBFBD><EFBFBD><EFBFBD>uProvisioning<6E>v<EFBFBD><76><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>O<EFBFBD><4F><EFBFBD>i<EFBFBD>g<EFBFBD>d<EFBFBD><64><EFBFBD>CGateway UI <20><><EFBFBD>~<7E><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><C3BB>i<EFBFBD><69><EFBFBD>F<EFBFBD><EFBFBD><EFA8AD>/Provisioning <20><><EFBFBD><EFBFBD><EFBFBD>ݨ<EFBFBD><DDA8>ӷ<EFBFBD><D3B7>t<EFBFBD>ΡC
2026-05-19 17:04:26 +00:00
### 5.4 <20>~<7E>ȯ<EFBFBD><C8AF><EFBFBD><EFBFBD>Ҽҫ<D2BC><D2AB>]<5D>w<EFBFBD>M<EFBFBD><4D><EFBFBD>^
2026-05-19 17:04:26 +00:00
```go
// Member <20>J<EFBFBD><4A> + <20><><EFBFBD>`<60>s<EFBFBD>W<EFBFBD><57><EFBFBD><EFBFBD>
2026-05-19 17:04:26 +00:00
type Member struct {
TenantID string
UID string
ZitadelUserID string // ZITADEL sub<75>]OIDC / LDAP IdP / platform local user <20><><EFBFBD>|<7C><><EFBFBD>^
ZitadelEmail string // <20>ӷ<EFBFBD> IdP <20><><EFBFBD>Ѫ<EFBFBD><D1AA>n<EFBFBD>J email
2026-05-19 17:04:26 +00:00
DisplayName string
Avatar string
Phone string
Language string
Currency string
Status enum.MemberStatus // unverified | active | suspended | deleted
Origin enum.MemberOrigin // platform_native | oidc | ldap | scim
PasswordHash string // <20><><EFBFBD>x<EFBFBD><78><EFBFBD>ͥB<CDA5><42><EFBFBD><EFBFBD> ZITADEL local user <20>ɤ~<7E><><EFBFBD>F<EFBFBD><46><EFBFBD>l<EFBFBD>d<EFBFBD><64>
2026-05-19 17:04:26 +00:00
BusinessEmail string // <20>~<7E><> email<69>]<5D>i<EFBFBD>P ZitadelEmail <20><><EFBFBD>P<EFBFBD>^
2026-05-19 17:04:26 +00:00
BusinessEmailVerified bool
BusinessEmailVerifiedAt int64
BusinessPhone string
BusinessPhoneVerified bool
BusinessPhoneVerifiedAt int64
TOTPEnrolled bool
TOTPSecretCipher string
TOTPEnrolledAt int64
TOTPBackupCodesHash []string
CreateAt int64
UpdateAt int64
DeletedAt int64 // soft delete <20>ɶ<EFBFBD>
AnonymizedAt int64 // <20>ΦW<CEA6>Ʈɶ<C6AE>
2026-05-19 17:04:26 +00:00
}
```
> **Origin** <20><><EFBFBD>ȡG
> - `platform_native`<60>GGateway <20><><EFBFBD>x<EFBFBD><78><EFBFBD>͵<EFBFBD><CDB5>U<EFBFBD>]<5D>f<EFBFBD>t ZITADEL local user <20><> Gateway <20>ۺޱK<DEB1>X<EFBFBD>^
> - `oidc`<60>GSocial / ZITADEL Hosted UI <20><> IdP <20>Ӫ<EFBFBD>
> - `ldap`<60>G<EFBFBD>z<EFBFBD>L ZITADEL LDAP IdP <20><> Directory Sync
> - `scim`<60>GHR / Entra / Okta <20><><EFBFBD>e
2026-05-19 17:04:26 +00:00
> `Member.Origin` <20>M<EFBFBD>w Profile <20><><EFBFBD><EFBFBD> UI <20>i<EFBFBD>g<EFBFBD>d<EFBFBD><64><EFBFBD>G
> - `zitadel_local`<60>G<EFBFBD><47><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]IdP email/name<6D>^<5E><>Ū<EFBFBD>A<EFBFBD>ݨ<EFBFBD> ZITADEL UI <20><><EFBFBD>F<EFBFBD>~<7E><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>i<EFBFBD>g
> - `ldap`<60>G<EFBFBD><47><EFBFBD><EFBFBD> + provisioning <20><><EFBFBD><EFBFBD><EFBFBD>Ұ<EFBFBD>Ū<EFBFBD>]<5D><> Directory Sync <20><><EFBFBD>@<40>^<5E>F<EFBFBD>~<7E><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>i<EFBFBD>g
> - `scim`<60>G<EFBFBD><47><EFBFBD><EFBFBD> + provisioning <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> SCIM Provider <20><><EFBFBD>e<EFBFBD>A<EFBFBD><41>Ū<EFBFBD>F<EFBFBD>~<7E><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>i<EFBFBD>g
2026-05-19 17:04:26 +00:00
>
> `UserRole.Source` <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> `manual / zitadel / ldap / scim`<60>A<EFBFBD>v<EFBFBD>T sync replace <20>d<EFBFBD><64><EFBFBD>]<5D><> <20><>6.10<EFBFBD>^<5E>C<EFBFBD><43><EFBFBD>̦U<CCA6>q<EFBFBD><71>¾<EFBFBD>C
2026-05-19 17:04:26 +00:00
| <20><><EFBFBD><EFBFBD> | <20>ӷ<EFBFBD> | <20>γ~ |
2026-05-19 17:04:26 +00:00
|------|------|------|
| `ZitadelEmail`<EFBFBD>]<5D>J<EFBFBD><4A><EFBFBD>^ | OIDC claim | <20>n<EFBFBD>J<EFBFBD>b<EFBFBD><62><EFBFBD>ѧO<D1A7>A<EFBFBD><41><EFBFBD><EFBFBD><EFBFBD>~<7E>Ȧu<C8A6><75> |
| `BusinessEmail` | <20>~<7E><> API <20>j<EFBFBD>w + OTP | <20>~<7E>ȳq<C8B3><71><EFBFBD>B<EFBFBD>~<7E>Ȧu<C8A6><75><EFBFBD><EFBFBD><EFBFBD><EFBFBD> |
| `BusinessPhone` | <20>~<7E><> API <20>j<EFBFBD>w + OTP | SMS <20>q<EFBFBD><71><EFBFBD>BStep-up MFA <20>q<EFBFBD>D |
2026-05-19 17:04:26 +00:00
**Verification Challenge<67>]<5D><><EFBFBD>J Mongo<67>A<EFBFBD>Ȧs Redis<69>ATTL 5min<69>^<5E>G**
2026-05-19 17:04:26 +00:00
```go
type VerificationChallenge struct {
TenantID string
UID string
Kind enum.VerifyKind // email | phone | step_up
Target string // email/phone <20>ت<EFBFBD><D8AA>a<EFBFBD>Fstep_up <20><> action
2026-05-19 17:04:26 +00:00
CodeHash string // bcrypt(otp)
AttemptCnt int // <20><><EFBFBD>Ѧ<EFBFBD><D1A6>ơA<C6A1>W<EFBFBD>L MaxAttempts <20><> <20><>
2026-05-19 17:04:26 +00:00
ExpireAt int64 // epoch ms
CreateAt int64
}
```
### 5.5 OTP <20><EFBFBD>]<5D>w<EFBFBD>M<EFBFBD><4D><EFBFBD>G<EFBFBD>z<EFBFBD>L Notification Module<6C>^
2026-05-19 17:04:26 +00:00
<EFBFBD>~<7E><> / step-up OTP **<EFBFBD>@<40>ߨ<EFBFBD>** `notification.NotifierUseCase`<EFBFBD>A**<2A><>**<2A>b member <20>Ҳժ<D2B2><D5AA><EFBFBD><EFBFBD><EFBFBD> provider SDK<44>CNotification module <20>Τ@<40>B<EFBFBD>z provider <20><><EFBFBD><EFBFBD><EFBFBD>B<EFBFBD>ҪO<D2AA>Bidempotency<63>B<EFBFBD><42><EFBFBD>աBaudit<69>]<5D><> <20><>11<31>^<5E>C
2026-05-19 17:04:26 +00:00
```go
// member.VerificationUseCase <20><><EFBFBD>I<EFBFBD>s
2026-05-19 17:04:26 +00:00
nu.Notifier.Send(ctx, &notification.SendRequest{
TenantID: tenantID,
UID: uid,
Channel: enum.ChannelEmail,
Kind: enum.NotifyVerifyEmail,
Target: targetEmail,
Locale: member.Language,
Data: map[string]any{"code": otp, "expires_in": 300},
IdempotencyKey: challengeID, // <20>P challenge <20><><EFBFBD>|<7C><><EFBFBD>o
DoNotPersistBody: true, // OTP <20><><EFBFBD>J notification.body
2026-05-19 17:04:26 +00:00
Severity: enum.SeverityInfo,
})
```
- **OTP <20>W<EFBFBD><57>**<2A>G6 <20><><EFBFBD>ơBTTL 5min<69>Bbcrypt <20>x<EFBFBD>s<EFBFBD>]<5D><><EFBFBD>s<EFBFBD><73><EFBFBD>X<EFBFBD>^<5E>B<EFBFBD><42><EFBFBD>o<EFBFBD>N<EFBFBD>o 60s<30>B<EFBFBD><42><EFBFBD>@ challenge <20><><EFBFBD><EFBFBD> 5 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
- **Rate Limit**<2A>G
- `verify:rate:{tenant}:{uid}:{kind}` SETNX TTL=60s<30>]<5D><><EFBFBD>o<EFBFBD>O<EFBFBD>@<40>^
- `verify:daily:{tenant}:{uid}:{kind}` INCR TTL=24h<34>]<5D><><EFBFBD><EFBFBD><EFBFBD>W<EFBFBD><57><EFBFBD>A<EFBFBD>w<EFBFBD>] 10 <20><><EFBFBD>^
- **Audit**<2A>GStart / Confirm <20>i audit log<6F>]Notification <20>ۤv<DBA4>]<5D>|<7C>O<EFBFBD>e<EFBFBD>F<EFBFBD><46><EFBFBD>A<EFBFBD>A<EFBFBD><41><EFBFBD>̤<EFBFBD><CCA4>ɡ^
- **Provider <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>v<EFBFBD>T member <20>Ҳ<EFBFBD>**<2A>G<EFBFBD><47> SendGrid <20><> SES<45>BTwilio <20><> SNS <20>u<EFBFBD><75> `etc/gateway.yaml` <20>P library <20><><EFBFBD>@
2026-05-19 17:04:26 +00:00
### 5.6 Step-up MFA<46>]<5D>w<EFBFBD>M<EFBFBD><4D><EFBFBD>G<EFBFBD>ҥΡ^
2026-05-19 17:04:26 +00:00
**<2A>γ~**<2A>G<EFBFBD><47><EFBFBD><EFBFBD><EFBFBD>I<EFBFBD>~<7E>Ⱦާ@<40>e<EFBFBD><65><EFBFBD>G<EFBFBD><47><EFBFBD><EFBFBD><EFBFBD>ҡA<D2A1>P ZITADEL <20><><EFBFBD><EFBFBD> MFA **<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>N**<2A>C
2026-05-19 17:04:26 +00:00
#### <20><><EFBFBD><EFBFBD><EFBFBD>I Action <20>M<EFBFBD><4D><EFBFBD>]enum<75>^
2026-05-19 17:04:26 +00:00
| Action | <20>ؼ<EFBFBD> API |
2026-05-19 17:04:26 +00:00
|--------|---------|
| `change_business_email` | `PATCH /members/me/business-email` |
| `change_business_phone` | `PATCH /members/me/business-phone` |
| `delete_member` | `DELETE /members/me` |
| `tenant_admin_force_status` | `PATCH /members/:uid/status`<EFBFBD>]<5D>޲z<DEB2><7A><EFBFBD><EFBFBD><EFBFBD>v<EFBFBD>L<EFBFBD>H<EFBFBD>^|
2026-05-19 17:04:26 +00:00
| `revoke_all_sessions` | `POST /auth/revoke-all` |
| `disable_totp` | `DELETE /members/me/totp` |
> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>i<EFBFBD><EFBFBD> tenant <20>z<EFBFBD>L<EFBFBD>]<5D>w<EFBFBD>[<5B>զW<D5A6><57><EFBFBD>F<EFBFBD>쪩 platform-wide enum<75>A<EFBFBD>T<EFBFBD><54><EFBFBD><EFBFBD><EFBFBD>N<EFBFBD>r<EFBFBD><72><EFBFBD>C
2026-05-19 17:04:26 +00:00
#### Step-up <20>q<EFBFBD>D<EFBFBD>]<5D>w<EFBFBD>M<EFBFBD><4D><EFBFBD>^
2026-05-19 17:04:26 +00:00
<EFBFBD>u<EFBFBD><EFBFBD><EFBFBD>ǡG**TOTP > SMS > Email**
2026-05-19 17:04:26 +00:00
| <20>q<EFBFBD>D | <20><><EFBFBD><EFBFBD> | <20><><EFBFBD><EFBFBD><EFBFBD>u<EFBFBD><75> |
2026-05-19 17:04:26 +00:00
|------|------|---------|
| **TOTP**<EFBFBD>]Google Authenticator<6F>^ | <20>ϥΪ̤w `enroll_totp` <20><><EFBFBD><EFBFBD><EFBFBD>]<5D><>5.8<EFBFBD>^ | <20><><EFBFBD>̿<EFBFBD><CCBF>~<7E><> provider<65>B<EFBFBD><42><EFBFBD>|<7C>Q SIM swap<61>B<EFBFBD>L<EFBFBD>W<EFBFBD>e<EFBFBD><65><EFBFBD><EFBFBD><EFBFBD>B<EFBFBD>s<EFBFBD><73><EFBFBD><EFBFBD> |
| **SMS** | `BusinessPhoneVerified = true` | <20><> email <20>Y<EFBFBD>ɡB<C9A1><42><EFBFBD><EFBFBD><EFBFBD>Q<EFBFBD>d<EFBFBD>I |
| **Email** | `BusinessEmailVerified = true` | <20><><EFBFBD>Ƴq<C6B3>D |
2026-05-19 17:04:26 +00:00
Start <20>ɥ<EFBFBD> `StepUpUseCase` <20>̨ϥΪ̪<CEAA><CCAA>A<EFBFBD>D<EFBFBD>q<EFBFBD>D<EFBFBD>F<EFBFBD>Y<EFBFBD>ϥΪ̭n<CCAD>D<EFBFBD><44><EFBFBD>L<EFBFBD>q<EFBFBD>D<EFBFBD>]<5D>p<EFBFBD><70><EFBFBD>Q<EFBFBD><51> TOTP<54>^<5E>i<EFBFBD>b request <20>a `prefer_channel` <20>мg<D0BC>A<EFBFBD><41><EFBFBD><EFBFBD><EFBFBD>ݸӳq<D3B3>D<EFBFBD>w<EFBFBD><77><EFBFBD>ҡC
2026-05-19 17:04:26 +00:00
#### <20>y<EFBFBD>{
2026-05-19 17:04:26 +00:00
```
1. Client <20><> POST /auth/step-up/start { action, prefer_channel?: "totp" }
- <20>ѪR<D1AA>ϥΪ̤w<CCA4>i<EFBFBD>γq<CEB3>D<EFBFBD>F<EFBFBD>D<EFBFBD><44><EFBFBD>u<EFBFBD><75><EFBFBD>q<EFBFBD>D
- <20>Y<EFBFBD><59> totp<74>G<EFBFBD><47><EFBFBD>H OTP<54>A<EFBFBD><41><EFBFBD><EFBFBD><EFBFBD>^ challenge_id<69>Fcode <20>ѨϥΪ̱q app <20><>
- <20>Y<EFBFBD><59> sms/email<69>G<EFBFBD>ͦ<EFBFBD> 6 <20>X OTP<54>Bbcrypt <20>x<EFBFBD>s<EFBFBD>B<EFBFBD>z<EFBFBD>L NotifierUseCase.Send <20>H<EFBFBD>X
<20><> { challenge_id, channel: "totp"|"sms"|"email", expires_in: 300 }
2026-05-19 17:04:26 +00:00
2. Client <20><> POST /auth/step-up/confirm { challenge_id, code, action }
- totp<74>Gmember.TOTPUseCase.VerifyCode(uid, code, window=<3D><>1)
- sms/email<69>Gbcrypt <20><><EFBFBD><EFBFBD> challenge code<64>F<EFBFBD><46><EFBFBD><EFBFBD> INCR AttemptCnt
- <20><><EFBFBD>\ <20><> auth.StepUpTokenUseCase.Issue(tenant, uid, action) <20><> <20>u<EFBFBD><75> JWT
<20><> { step_up_token, token_type: "step_up", expires_in: 300 }
2026-05-19 17:04:26 +00:00
3. Client <20><> PATCH /members/me/business-email { ... }
2026-05-19 17:04:26 +00:00
Header: X-Step-Up-Token: <step_up_token>
- Logic <20>h<EFBFBD>G
a. Casbin enforce <20>q<EFBFBD>L<EFBFBD>]<5D><><EFBFBD><EFBFBD><EFBFBD>v<EFBFBD><76><EFBFBD>^
2026-05-19 17:04:26 +00:00
b. StepUpTokenUseCase.Verify(token, expectedAction="change_business_email", tenant, uid)
c. SETNX auth:stepup:used:{jti}=1<>A<EFBFBD>w<EFBFBD>ιL <20><> <20>ڵ<EFBFBD>
d. <20><><EFBFBD><EFBFBD><EFBFBD>~<7E><><EFBFBD>޿<EFBFBD>
2026-05-19 17:04:26 +00:00
```
#### <20>u<EFBFBD><75><EFBFBD>I
2026-05-19 17:04:26 +00:00
- Logic <20>h<EFBFBD>u<EFBFBD><75><EFBFBD>G**Casbin allow <20><>**<2A>A<EFBFBD><41> step-up<75>F<EFBFBD><46><EFBFBD>h<EFBFBD><68>
- Header <20>W<EFBFBD>١G`X-Step-Up-Token`
- <20><><EFBFBD>Ѧ^<5E>ǡG`403 step_up_required` + `{ required_action: "change_business_email", available_channels: ["totp","sms"] }`<EFBFBD>A<EFBFBD>e<EFBFBD>ݨ̦<EFBFBD><EFBFBD><EFBFBD> step-up <20>y<EFBFBD>{
2026-05-19 17:04:26 +00:00
### 5.7 <20>b<EFBFBD><62><EFBFBD>R<EFBFBD><52><EFBFBD>P<EFBFBD>ΦW<CEA6>ơ]<5D>w<EFBFBD>M<EFBFBD><4D><EFBFBD>^
2026-05-19 17:04:26 +00:00
```
T0: DELETE /api/v1/members/me (Step-up: delete_member)
1. status = deleted, deleted_at = now
2. auth.RevokeAllForUser<65>]INCR auth_gen + <20><> jti pair <20>¦W<C2A6><57><EFBFBD>^
2026-05-19 17:04:26 +00:00
3. ZITADEL Mgmt.DeactivateUser
4. <20>M member:profile / member:sub cache
2026-05-19 17:04:26 +00:00
5. audit log (actor, ip, ua, step_up_jti)
T+30 <20><>: cron `member_anonymize_worker`
<20>ΦW<CEA6><57><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]<5D>мg<D0BC><67> hash <20>ΩT<CEA9>w placeholder<65>^:
ZitadelEmail <20><> "deleted:{uid}@anonymized.local"
DisplayName <20><> "Deleted User"
Avatar <20><> ""
Phone <20><> ""
BusinessEmail <20><> ""
BusinessPhone <20><> ""
BusinessEmail/PhoneVerified <20><> false
TOTPSecretCipher <20><> ""
TOTPBackupCodesHash <20><> nil
external_id, ldap_dn <20><> ""
zitadel_sub <20><> "deleted:{uid}" # <20><><EFBFBD><EFBFBD> identities <20>ߤ@<40><><EFBFBD><EFBFBD>
<20>O<EFBFBD>d<EFBFBD><64><EFBFBD><EFBFBD><EFBFBD>]<5D><><EFBFBD>i<EFBFBD><69> / <20>f<EFBFBD>p<EFBFBD>Ρ^:
2026-05-19 17:04:26 +00:00
tenant_id, uid, status=deleted, deleted_at, anonymized_at, created_at
<20>g audit log: action=member.anonymized
2026-05-19 17:04:26 +00:00
```
- **<2A><><EFBFBD>i<EFBFBD>f**<2A>F30 <20>Ѥ<EFBFBD><D1A4>i<EFBFBD>ѯ<EFBFBD><D1AF><EFBFBD> admin <20>٭<EFBFBD><D9AD>]`status=deleted <20><> active`<60>A<EFBFBD><41><EFBFBD>_ cache<68>A<EFBFBD><41> ZITADEL <20>b<EFBFBD><62><EFBFBD>ݥt<DDA5><74><EFBFBD>ҥΡ^
- audit log <20><><EFBFBD><EFBFBD><EFBFBD>ΦW<CEA6>Ƽv<C6BC>T<EFBFBD>]actor uid <20><><EFBFBD>O<EFBFBD>d<EFBFBD>A<EFBFBD>K<EFBFBD><4B><EFBFBD>l<EFBFBD><6C><EFBFBD>^
- <20>ΦW<CEA6>ƫ<EFBFBD> SCIM `Users.{id}` <20><><EFBFBD>i<EFBFBD>d<EFBFBD><64><EFBFBD>]<5D>^<5E><> `active=false` + <20>ΦW payload<61>^<5E>A<EFBFBD><41><EFBFBD>^ 404<30>A<EFBFBD>H<EFBFBD><48><EFBFBD><EFBFBD> client <20><> reconciliation
2026-05-19 17:04:26 +00:00
### 5.8 TOTP<54>]Authenticator App<70>A<EFBFBD>w<EFBFBD>M<EFBFBD><4D><EFBFBD>G<EFBFBD>ҥΡ^
2026-05-19 17:04:26 +00:00
<EFBFBD>~<7E>ȯ<EFBFBD> TOTP<54>AGateway **<EFBFBD>ۤv<EFBFBD>s secret**<2A>A<EFBFBD>P ZITADEL <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> TOTP **<EFBFBD>W<EFBFBD><EFBFBD>**<2A>]<5D><><EFBFBD>ӿW<D3BF>߸j<DFB8>w<EFBFBD>A<EFBFBD>ϥΪ̭<CEAA><CCAD><EFBFBD> setup <20>ݦU<DDA6><55><EFBFBD>@<40><> QR<51>^<5E>C
2026-05-19 17:04:26 +00:00
> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>}<7D>HZITADEL TOTP <20>O<EFBFBD>n<EFBFBD>J<EFBFBD>ΡBsecret <20>b ZITADEL<45>FGateway step-up TOTP <20>Ω<EFBFBD><CEA9>~<7E>Ⱦާ@<40>Bsecret <20>b Gateway<61>A<EFBFBD>קK Gateway <20><> ZITADEL <20>p<EFBFBD><70><EFBFBD><EFBFBD><EFBFBD>ƪ<EFBFBD><C6AA>̿<EFBFBD><CCBF>P<EFBFBD><50><EFBFBD>X<EFBFBD>C
2026-05-19 17:04:26 +00:00
#### Member <20><><EFBFBD><EFBFBD><EFBFBD>]<5D>ɥR <20><>5.4<EFBFBD>^
2026-05-19 17:04:26 +00:00
```go
type Member struct {
// ... <20>J<EFBFBD><4A><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
2026-05-19 17:04:26 +00:00
TOTPEnrolled bool
TOTPSecretCipher string // AES-GCM(secret, KEK)<29>AAES-256<35>FKEK <20><> KMS / secret manager
2026-05-19 17:04:26 +00:00
TOTPEnrolledAt int64
TOTPBackupCodesHash []string // bcrypt(code)<29>A10 <20>դ@<40><><EFBFBD>ʳƴ<CAB3><C6B4>X<EFBFBD>A<EFBFBD>ιL<CEB9>Y<EFBFBD>ٰ<EFBFBD>
2026-05-19 17:04:26 +00:00
}
```
> Secret <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>٥[<5B>K<EFBFBD>x<EFBFBD>s<EFBFBD>A**<2A>T<EFBFBD><54>**<2A><><EFBFBD>X<EFBFBD>γ<EFBFBD><CEB3><EFBFBD> base32<33>CKEK <20><> KMS / Vault<6C>Frotation <20>ɳv<C9B3><76> re-encrypt<70>]<5D>I<EFBFBD><49> worker<65>^<5E>C
2026-05-19 17:04:26 +00:00
#### UseCase <20><><EFBFBD><EFBFBD><EFBFBD>]<5D>ɥR <20><>5.2<EFBFBD>^
2026-05-19 17:04:26 +00:00
```go
type TOTPUseCase interface {
// <20><><EFBFBD><EFBFBD> secret + otpauth URL + 10 <20><> backup codes<65>]<5D><><EFBFBD><EFBFBD> enroll<6C>F<EFBFBD>|<7C><><EFBFBD>ҥΡ^
2026-05-19 17:04:26 +00:00
StartEnroll(ctx context.Context, tenantID, uid string) (*EnrollStartDTO, error)
// <20>ϥΪ̱<CEAA> QR<51>B<EFBFBD><42><EFBFBD>J<EFBFBD>Ĥ@<40><> code <20><> <20>T<EFBFBD>{ <20><> <20><> TOTPEnrolled = true
ConfirmEnroll(ctx context.Context, tenantID, uid, code string) ([]string, error) // <20>^ backup_codes<65>]<5D><><EFBFBD>X<EFBFBD>A<EFBFBD>u<EFBFBD>^<5E>@<40><><EFBFBD>^
// step-up <20>ΡG<CEA1><47><EFBFBD>@<40><> code<64>]<5D>t backup code<64>^
2026-05-19 17:04:26 +00:00
VerifyCode(ctx context.Context, tenantID, uid, code string) error
// <20>Ѱ<EFBFBD><D1B0>j<EFBFBD>w<EFBFBD>]<5D><> step-up = disable_totp<74>^
2026-05-19 17:04:26 +00:00
Disable(ctx context.Context, tenantID, uid string) error
// <20><><EFBFBD>s<EFBFBD><73><EFBFBD><EFBFBD> backup codes<65>]<5D><> step-up<75>^
2026-05-19 17:04:26 +00:00
RegenerateBackupCodes(ctx context.Context, tenantID, uid string) ([]string, error)
}
```
#### <20>y<EFBFBD>{
2026-05-19 17:04:26 +00:00
```
A. Enroll
Client <20><> POST /api/v1/members/me/totp/enroll-start
1. <20>Y<EFBFBD>w TOTPEnrolled = true <20><> 409 already_enrolled
2. <20>ͦ<EFBFBD> 32-byte random secret <20><> base32
2026-05-19 17:04:26 +00:00
3. otpauth_url = "otpauth://totp/{Issuer}:{tenant_slug}:{uid}?secret={base32}&issuer={Issuer}&algorithm=SHA1&digits=6&period=30"
4. <20>Ȧs<C8A6><73> Redis<69>]<5D><><EFBFBD>J Mongo<67>A<EFBFBD>קK<D7A7>b<EFBFBD><62><EFBFBD><EFBFBD><EFBFBD><EFBFBD> secret <20><><EFBFBD><EFBFBD><EFBFBD>^:
2026-05-19 17:04:26 +00:00
totp:enroll:{tenant}:{uid} = {secret_cipher} TTL 10min
<20><> { otpauth_url, qr_png_base64 }
2026-05-19 17:04:26 +00:00
Client <20><> POST /api/v1/members/me/totp/enroll-confirm { code }
1. <20>q Redis <20><><EFBFBD>Ȧs secret
2. VerifyTOTP(secret, code, window=<3D><>1) <20><> <20><><EFBFBD>ѫh 400 invalid_code
2026-05-19 17:04:26 +00:00
3. member.TOTPSecretCipher = secret_cipher
member.TOTPEnrolled = true
member.TOTPEnrolledAt = now
4. <20>ͦ<EFBFBD> 10 <20><> backup code (random hex)<29>Bbcrypt <20><><EFBFBD>s TOTPBackupCodesHash
2026-05-19 17:04:26 +00:00
5. DEL totp:enroll:*
6. audit log
<20><> { backup_codes: [...10 <20>թ<EFBFBD><D5A9>X<EFBFBD>A<EFBFBD>Ȧ<EFBFBD><C8A6>@<40><><EFBFBD>^<5E><>] }
2026-05-19 17:04:26 +00:00
B. Verify<66>]step-up <20>@<40>Ρ^
StepUpUseCase.Confirm <20><><EFBFBD>G
VerifyTOTP(decryptedSecret, code, window=<3D><>1) OR matchBackupCode(code)
<20>Y<EFBFBD><59> backup code <20>R<EFBFBD><52> <20><> <20>q TOTPBackupCodesHash <20><><EFBFBD><EFBFBD><EFBFBD>ӵ<EFBFBD><D3B5>]<5D><EFBFBD>ʡ^
2026-05-19 17:04:26 +00:00
C. Disable
Client <20><> DELETE /api/v1/members/me/totp
2026-05-19 17:04:26 +00:00
Header: X-Step-Up-Token: <action=disable_totp>
1. <20>M TOTPSecretCipher<65>BTOTPEnrolled=false<73>BTOTPBackupCodesHash=nil
2026-05-19 17:04:26 +00:00
2. audit log
```
#### TOTP <20>t<EFBFBD><74><EFBFBD>k<EFBFBD>P<EFBFBD>Ѽ<EFBFBD>
2026-05-19 17:04:26 +00:00
- **RFC 6238**<2A>]SHA1 / 30s period / 6 digits<74>^<5E>A<EFBFBD>ۮe Google Authenticator<6F>BAuthy<68>B1Password<72>BMicrosoft Authenticator
- `window = <20><>1`<EFBFBD>G<EFBFBD><EFBFBD><EFBFBD>\<5C>e<EFBFBD><65><EFBFBD>@<40><> 30s <20>϶<EFBFBD><CFB6>A<EFBFBD>e<EFBFBD>Ԯ<EFBFBD><D4AE><EFBFBD><EFBFBD>}<7D><>
- Replay <20>O<EFBFBD>@<40>G<EFBFBD><47><EFBFBD>\<5C>ϥΪ<CFA5> `(uid, code, timestep)` <20>g<EFBFBD>J `totp:used:{tenant}:{uid}:{timestep}` SETNX TTL=90s<30>F<EFBFBD>P<EFBFBD>@ code <20>G<EFBFBD><47><EFBFBD>X<EFBFBD>{<7B><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
- Backup code<64>G10 <20>աB12 <20>r hex<65>]48-bit entropy<70>^<5E>Bbcrypt cost 10<31>B<EFBFBD><42><EFBFBD>X<EFBFBD><58> enroll <20>ɦ^<5E>Ǥ@<40><>
2026-05-19 17:04:26 +00:00
#### API<50>]<5D>ɥR <20><>7.2<EFBFBD>^
2026-05-19 17:04:26 +00:00
| Method | Path | <20><><EFBFBD><EFBFBD> | Step-up |
2026-05-19 17:04:26 +00:00
|--------|------|------|---------|
| POST | `/api/v1/members/me/totp/enroll-start` | <20><> otpauth URL + QR | <20>X |
| POST | `/api/v1/members/me/totp/enroll-confirm` | <20><><EFBFBD>Ĥ@<40><> code<64>A<EFBFBD>ҥ<EFBFBD> + <20>^ backup codes | <20>X |
| GET | `/api/v1/members/me/totp` | <20><> TOTP <20><><EFBFBD>A<EFBFBD>]enrolled? backup <20>Ѿl<D1BE>ơ^ | <20>X |
| POST | `/api/v1/members/me/totp/backup-codes` | <20><><EFBFBD><EFBFBD> backup codes | ? `disable_totp` |
| DELETE | `/api/v1/members/me/totp` | <20>Ѱ<EFBFBD><D1B0>j<EFBFBD>w | ? `disable_totp` |
2026-05-19 17:04:26 +00:00
### 5.9 UseCase <20>s<EFBFBD>ƥܨҡ]<5D>·<EFBFBD><C2B7><EFBFBD><EFBFBD>Fhandler / API <20>Ȥ<EFBFBD><C8A4><EFBFBD><EFBFBD>@<40>^
2026-05-19 17:04:26 +00:00
> <EFBFBD>i<EFBFBD><EFBFBD> atomic primitives <20>i<EFBFBD><69><EFBFBD>N<EFBFBD>զX<D5A6><58><EFBFBD>޿<EFBFBD><DEBF>y<EFBFBD>C**logic <20>h<EFBFBD>|<7C><><EFBFBD><EFBFBD><EFBFBD>@**<2A>F<EFBFBD><46><EFBFBD>`<60><><EFBFBD>ҩ<EFBFBD><D2A9><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>i<EFBFBD><EFBFBD>w<EFBFBD><77><EFBFBD>~<7E>ȡC
2026-05-19 17:04:26 +00:00
#### Case A<>G<EFBFBD><47><EFBFBD>x<EFBFBD><78><EFBFBD>͵<EFBFBD><CDB5>U + Email OTP <20><><EFBFBD>ҡ]<5D><><EFBFBD>Ӹ<EFBFBD><D3B8>|<7C>^
2026-05-19 17:04:26 +00:00
```go
// 1) <20>إ<EFBFBD> unverified member<65>]<5D><><EFBFBD>H<EFBFBD>H<EFBFBD>B<EFBFBD><42><EFBFBD>o token<65>^
2026-05-19 17:04:26 +00:00
m, _ := mLifecycle.CreateUnverified(ctx, &CreatePlatformMemberRequest{
TenantID: tenantID, Email: email, DisplayName: name,
})
// 2) <20><><EFBFBD><EFBFBD> OTP<54>]atomic<69>Bpurpose-agnostic<69>^
2026-05-19 17:04:26 +00:00
chal, _ := mOTP.Generate(ctx, &GenerateOTPRequest{
TenantID: tenantID,
Purpose: OTPPurposeRegistrationEmail,
Identifier: m.UID,
})
// 3) <20>뻼 OTP<54>]atomic<69>Fcaller <20><><EFBFBD><EFBFBD> channel / template<74>^
2026-05-19 17:04:26 +00:00
notifier.Send(ctx, &SendRequest{
TenantID: tenantID,
UID: m.UID,
Channel: ChannelEmail,
Kind: NotifyVerifyRegistrationEmail,
Target: email,
Data: map[string]any{"code": chal.Code, "expires_in": chal.ExpiresIn},
IdempotencyKey: chal.ChallengeID,
DoNotPersistBody: true,
})
// <20>]<5D>ϥΪ̦<CEAA><CCA6>H<EFBFBD>B<EFBFBD><42><EFBFBD>J code <20><> <20><><EFBFBD>ݨ<EFBFBD><DDA8>H<EFBFBD>U<EFBFBD><55><EFBFBD>B<EFBFBD>^
2026-05-19 17:04:26 +00:00
// 4) <20><><EFBFBD><EFBFBD> OTP<54>]atomic<69>^
2026-05-19 17:04:26 +00:00
_ = mOTP.Verify(ctx, &VerifyOTPRequest{
TenantID: tenantID, ChallengeID: chal.ChallengeID,
Code: userCode, Purpose: OTPPurposeRegistrationEmail,
})
// 5) <20>ҥΡ]atomic<69>^<5E>Gunverified <20><> active
2026-05-19 17:04:26 +00:00
_ = mLifecycle.Activate(ctx, tenantID, m.UID)
```
#### Case B<>GOIDC<44>]Social / ZITADEL Hosted UI<55>^<5E>n<EFBFBD>J <20>X <20><><EFBFBD><EFBFBD> OTP
2026-05-19 17:04:26 +00:00
```go
m, _ := mProv.EnsureFromOIDC(ctx, &EnsureFromOIDCRequest{
TenantID: tenantID,
ZitadelSub: claims.Sub,
Email: claims.Email,
EmailVerified: claims.EmailVerified,
DisplayName: claims.Name,
})
// <20><><EFBFBD><EFBFBD> active<76>F<EFBFBD><46><EFBFBD><EFBFBD> auth.IssueTokenPair
2026-05-19 17:04:26 +00:00
```
#### Case C<>GLDAP IdP <20><><EFBFBD><EFBFBD><EFBFBD>n<EFBFBD>J JIT <20>X <20><><EFBFBD><EFBFBD> OTP
2026-05-19 17:04:26 +00:00
```go
m, _ := mProv.EnsureFromLDAP(ctx, &EnsureFromLDAPRequest{
TenantID: tenantID, ExternalID: ldapUUID, LDAPDN: dn,
Username: username, Email: email, DisplayName: name,
Groups: groups, Source: RoleSourceLDAPJIT,
})
```
#### Case D<>GSCIM Create User <20>X <20><><EFBFBD><EFBFBD> OTP
2026-05-19 17:04:26 +00:00
```go
m, _ := mProv.EnsureFromSCIM(ctx, &EnsureFromSCIMRequest{
TenantID: tenantID, ExternalID: scimExternalID,
UserName: username, Email: email, Active: true, RawPayload: rawJSON,
})
```
#### Case E<>G<EFBFBD>w<EFBFBD>n<EFBFBD>J user <20><><EFBFBD>j<EFBFBD>~<7E><> email<69>]atomic <20><><EFBFBD><EFBFBD> vs composite<74>^
2026-05-19 17:04:26 +00:00
```go
// <20><><EFBFBD>| 1<>G<EFBFBD><47><EFBFBD><EFBFBD><EFBFBD><EFBFBD> atomic<69>]<5D><><EFBFBD>ӱ<EFBFBD><D3B1><EFBFBD><EFBFBD>ɥΡ^
2026-05-19 17:04:26 +00:00
chal, _ := mOTP.Generate(ctx, &GenerateOTPRequest{
TenantID: tenantID, Purpose: OTPPurposeBusinessEmail, Identifier: uid,
})
notifier.Send(ctx, &SendRequest{
Channel: ChannelEmail, Kind: NotifyVerifyBusinessEmail,
Target: newEmail, Data: map[string]any{"code": chal.Code},
IdempotencyKey: chal.ChallengeID, DoNotPersistBody: true,
})
_ = mOTP.Verify(ctx, &VerifyOTPRequest{
TenantID: tenantID, ChallengeID: chal.ChallengeID,
Code: userCode, Purpose: OTPPurposeBusinessEmail,
})
_ = mProfile.SetBusinessEmailVerified(ctx, tenantID, uid, newEmail)
// <20><><EFBFBD>| 2<>G<EFBFBD><47> composite<74><><EFBFBD>p<EFBFBD><70><EFBFBD>o<EFBFBD>ӴN<D3B4>n<EFBFBD>^
2026-05-19 17:04:26 +00:00
chal, _ := mVerification.StartEmailVerify(ctx, tenantID, uid, newEmail)
// ...
_ = mVerification.ConfirmEmailVerify(ctx, tenantID, uid, chal.ChallengeID, userCode)
```
> <EFBFBD>C<EFBFBD><EFBFBD> atomic <20>ʧ@<40>W<EFBFBD>ߥi<DFA5>I<EFBFBD>s<EFBFBD>B<EFBFBD>W<EFBFBD><57> audit<69>B<EFBFBD>W<EFBFBD>ߥ<EFBFBD><DFA5>ѭ<EFBFBD><D1AD>աCLogic <20>ۦ<EFBFBD><DBA6>M<EFBFBD>w<EFBFBD>զX<D5A6>P<EFBFBD><50><EFBFBD>ǡC
2026-05-19 13:56:59 +00:00
---
## 6. permission <20>Ҳա]B2B <20>۩w<DBA9>q<EFBFBD>A<EFBFBD>Ѧ<EFBFBD> permission-server<65>^
2026-05-19 13:56:59 +00:00
<EFBFBD><EFBFBD><EFBFBD>|<7C>G`internal/model/permission/`
2026-05-19 13:56:59 +00:00
> <EFBFBD><EFBFBD><EFBFBD>`<60>l<EFBFBD><6C> [app-cloudep-permission-server](https://code.30cm.net/digimon/app-cloudep-permission-server) <20>w<EFBFBD><77><EFBFBD>Ҫ<EFBFBD><D2AA>]<5D>p<EFBFBD>G**Casbin + Redis RBAC**<2A>B**Permission Tree<65>]<5D><><EFBFBD>l<EFBFBD>~<7E>ӡ^**<2A>B**HTTP Path/Method <20>j<EFBFBD>w**<2A>C
> **<2A>P<EFBFBD><50> permission-server <20><><EFBFBD>t<EFBFBD><74>**<2A>GToken ñ<>o/<2F><><EFBFBD><EFBFBD>/<2F>¦W<C2A6><EFBFBD><E6B2BE> Gateway `auth` <20>ҲաF`ClientID` <20>אּ `tenant_id`<60>F<EFBFBD><EFBFBD>h<EFBFBD><68><EFBFBD><EFBFBD> B2B <20>U<EFBFBD>۩w<DBA9>q Role<6C>C
2026-05-19 13:56:59 +00:00
### 6.1 <20>]<5D>p<EFBFBD>ؼ<EFBFBD>
2026-05-19 13:56:59 +00:00
| <20><><EFBFBD>O | <20><><EFBFBD><EFBFBD> |
2026-05-19 13:56:59 +00:00
|------|------|
| **Permission Tree** | <20><><EFBFBD><EFBFBD><EFBFBD>v<EFBFBD><76><EFBFBD><EFBFBD><EFBFBD>]<5D><><EFBFBD>x seed<65>^<5E>A<EFBFBD><41><EFBFBD>l<EFBFBD>`<60>I<EFBFBD>~<7E>ӡF<D3A1><46><EFBFBD>`<60>I<EFBFBD><49><EFBFBD><EFBFBD><EFBFBD>h<EFBFBD>l<EFBFBD>`<60>I<EFBFBD><49><EFBFBD>i<EFBFBD><69> |
| **Casbin RBAC** | <20>H `(tenant_id, role_key, http_path, http_method)` <20><> API <20><><EFBFBD>v<EFBFBD>Fpath <20>`keyMatch2` <20>U<EFBFBD>Φr<CEA6><72> |
| **B2B <20>۩w<DBA9>q Role** | <20>C<EFBFBD>ӯ<EFBFBD><D3AF><EFBFBD><EFBFBD>إߦۭq Role<6C>A<EFBFBD>q<EFBFBD><71><EFBFBD><EFBFBD> Catalog **<EFBFBD>Ŀ<EFBFBD>** Permission<6F>]<5D><><EFBFBD>i<EFBFBD>۳<EFBFBD> Permission <20>r<EFBFBD><72><EFBFBD>^ |
| **UserRole** | <20><><EFBFBD><EFBFBD> + uid + role<6C>F<EFBFBD><EFBFBD>h<EFBFBD><68><EFBFBD><EFBFBD><EFBFBD>]<5D>H immutable role key <20><> Casbin subject<63>^ |
| **RolePermission** | <20>Ŀ<EFBFBD><C4BF>l<EFBFBD>v<EFBFBD><76><EFBFBD>ɦ۰ʸɻ<CAB8><C9BB><EFBFBD><EFBFBD>v<EFBFBD><76> ID<49>]<5D>u<EFBFBD><75> permission-server <20><> `getFullParentPermissionIDs`<EFBFBD>^ |
| **Policy <20>P<EFBFBD>B** | MongoDB <20><> Casbin Policy <20><> Redis<69>F<EFBFBD>w<EFBFBD><77> `LoadPolicy` + <20>ܧ<EFBFBD><DCA7><EFBFBD>IJ<EFBFBD>o reload |
| **<EFBFBD>~<7E><><EFBFBD>M<EFBFBD>g** | ZITADEL Role / LDAP Group / SCIM Group <20><> <20><><EFBFBD><EFBFBD><E1A4BA> Role.Key |
| **<EFBFBD>Ӳɫ<EFBFBD><EFBFBD>X<EFBFBD>i** | <20>P<EFBFBD>@ API <20>i<EFBFBD><69> `.plain_code` <20>l<EFBFBD>v<EFBFBD><76><EFBFBD>]<5D>p<EFBFBD><70><EFBFBD>X<EFBFBD>d<EFBFBD>ߡ^<5E>A<EFBFBD>u<EFBFBD><75><EFBFBD>³]<5D>p |
### 6.2 <20>P app-cloudep-permission-server <20><><EFBFBD><EFBFBD>
| permission-server | Gateway permission <20>Ҳ<EFBFBD> | <20>Ƶ<EFBFBD> |
2026-05-19 13:56:59 +00:00
|-------------------|---------------------------|------|
| `TokenService` | **`auth` <20>Ҳ<EFBFBD>** | JWT <20><><EFBFBD>A<EFBFBD><41> permission-server |
| `PermissionService`<EFBFBD>]<5D>š^ | <20><><EFBFBD><EFBFBD> HTTP API | <20><><EFBFBD><EFBFBD><EFBFBD>b Gateway <20><><EFBFBD>S |
| `entity.Permission` | <20>u<EFBFBD><75> + `tenant_id` <20><><EFBFBD>A<EFBFBD>Ρ]<5D><><EFBFBD><EFBFBD> Catalog<6F>^ | Permission <20><><EFBFBD><EFBFBD><EFBFBD>x<EFBFBD><78> |
| `entity.Role.ClientID` | `Role.TenantID` | <20><><EFBFBD><EFBFBD><EFBFBD>j<EFBFBD><6A> |
| `entity.Role.UID` | `Role.CreatorUID` | <20>إߪ̡A<CCA1>i<EFBFBD><69> |
| `entity.Role.Name` | `Role.DisplayName` | <20><><EFBFBD>ܦW<DCA6>١A<D9A1>i<EFBFBD><69><EFBFBD>W |
| <20>X | `Role.Key` | **Casbin policy <20><> role <20><><EFBFBD><EFBFBD>**<EFBFBD>A<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ߤ@<40>B<EFBFBD><42><EFBFBD>i<EFBFBD><69> |
| `Casbin Enforcer` | `RBACUseCase` + Redis Adapter | <20>u<EFBFBD><75> |
| `PermissionTree` | `usecase/permission_tree.go` | <20>u<EFBFBD><75> |
| `AdminRoleUID` / `GodDog` | `PlatformAdminRoleKey` + allowlist | <20><><EFBFBD>x<EFBFBD>W<EFBFBD>ź޲z<DEB2><7A> bypass<73>A<EFBFBD><41> audit |
2026-05-19 13:56:59 +00:00
| `permission.Type` | `enum.PermissionType` | `BackendUser` / `FrontendUser` |
### 6.3 <20>֤߷<D6A4><DFB7><EFBFBD>
2026-05-19 13:56:59 +00:00
```
Permission<EFBFBD>]<5D><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>^ <20><><EFBFBD>x<EFBFBD>w<EFBFBD>q<EFBFBD>A<EFBFBD>t name / http_path / http_method / parent / status / type
Role<EFBFBD>]<5D><><EFBFBD><EFBFBD><EFBFBD>۩w<DBA9>q<EFBFBD>^ <20><><EFBFBD><EFBFBD><EFBFBD>إߪ<D8A5><DFAA><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Fdisplay_name <20>i<EFBFBD><69><EFBFBD>Akey <20><><EFBFBD>i<EFBFBD><69><EFBFBD>A<EFBFBD>p sales_supervisor<6F>Btenant_admin
RolePermission Role ? Permission ID <20>h<EFBFBD><68><EFBFBD>h<EFBFBD>F<EFBFBD>Ŀ<EFBFBD><C4BF>ɦ۰ʸɤ<CAB8><C9A4>`<60>I
UserRole uid ? Role<6C>F<EFBFBD>@ user <20>i<EFBFBD>h role
RoleMapping <20>~<7E><> Group/Role <20><> <20><><EFBFBD><EFBFBD> RoleID / Role.Key
2026-05-19 17:04:26 +00:00
Casbin Policy p, {tenant_id}, {role_key}, {http_path}, {http_method}, {permission.Name}
2026-05-19 13:56:59 +00:00
```
### 6.4 Permission Entity<74>]<5D><><EFBFBD><EFBFBD> Catalog<6F>^
2026-05-19 13:56:59 +00:00
<EFBFBD>u<EFBFBD><EFBFBD> permission-server <20><> `entity.Permission` <20><><EFBFBD>c<EFBFBD>AMongoDB collection<6F>G`permission`<60>C
2026-05-19 13:56:59 +00:00
```go
type Permission struct {
2026-05-19 17:04:26 +00:00
ID primitive.ObjectID
Parent string // <20><><EFBFBD>v<EFBFBD><76> ID<49>]ObjectID hex<65>^<5E>F<EFBFBD><46> = <20><> root
Name string // <20>ߤ@<40>y<EFBFBD>N<EFBFBD>W<EFBFBD>Adot notation<6F>A<EFBFBD>p member.info.select
HTTPMethods string // <20><><EFBFBD>Ȧp "GET"<22>A<EFBFBD><41> regex <20>p "GET|POST|PATCH"<22>F<EFBFBD><46><EFBFBD><EFBFBD><EFBFBD>`<60>I<EFBFBD><49><EFBFBD><EFBFBD>
HTTPPath string // <20>p /api/v1/members/*<2A>]keyMatch2 pattern<72>^<5E>F<EFBFBD><46><EFBFBD><EFBFBD><EFBFBD>`<60>I<EFBFBD><49><EFBFBD><EFBFBD>
2026-05-19 17:04:26 +00:00
Status enum.Status // open | close
Type enum.PermissionType // backend_user | frontend_user<65>]<5D><><EFBFBD>x / <20>e<EFBFBD>x<EFBFBD><78><EFBFBD><EFBFBD><EFBFBD>^
2026-05-19 17:04:26 +00:00
CreateAt int64
UpdateAt int64
2026-05-19 13:56:59 +00:00
}
```
> **`Permission.Name` <20>@<40><><EFBFBD>إߤ<D8A5><DFA4>i<EFBFBD><69><EFBFBD>W**<2A>]<5D>Q RolePermission<6F>BUI i18n <20><><EFBFBD>BCasbin policy.name <20><><EFBFBD><EFBFBD><EFBFBD>ޥΡ^<5E>C
> <EFBFBD>o<EFBFBD><EFBFBD><EFBFBD><EFBFBD> `status=close`<60>F<EFBFBD>s<EFBFBD>W<EFBFBD>٥t<D9A5>طs leaf<61>C<EFBFBD><43><EFBFBD>R<EFBFBD>W<EFBFBD>n<EFBFBD><6E><EFBFBD><EFBFBD><EFBFBD>ƾE<C6BE><45><EFBFBD>}<7D><><EFBFBD>C
> **`HTTPPath` <20><><EFBFBD><EFBFBD>**<2A>G<EFBFBD>קK<D7A7>r `*`<60>F<EFBFBD>U<EFBFBD>θ<EFBFBD><CEB8>|<7C>n<EFBFBD><6E><EFBFBD>T<EFBFBD>ХX<D0A5><EFBFBD>ڡA<DAA1>Ҧp `/api/v1/members/*`<60>A<EFBFBD>T<EFBFBD><54> `/api/v1/*` <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>s<EFBFBD><73> pattern<72>]<5D><> keyMatch2 <20>g<EFBFBD><67><EFBFBD>R<EFBFBD><52><EFBFBD>^<5E>C
2026-05-19 17:04:26 +00:00
#### <20>R<EFBFBD>W<EFBFBD>W<EFBFBD>h<EFBFBD>]dot notation<6F>A<EFBFBD>P permission-server <20>@<40>P<EFBFBD>^
2026-05-19 13:56:59 +00:00
```
{domain}.{module}.{action}
{domain}.{module}.{action}.{variant} # <20>p .plain_code
2026-05-19 13:56:59 +00:00
```
#### Permission Tree <20>d<EFBFBD>ҡ]seed <20><><EFBFBD>ס^
2026-05-19 13:56:59 +00:00
```
member.info.management # <20>@<40>šG<C5A1>|<7C><><EFBFBD><EFBFBD><EFBFBD>T<EFBFBD>޲z<DEB2>]<5D><><EFBFBD><EFBFBD><EFBFBD>A<EFBFBD>L HTTP<54>^
<EFBFBD>u<EFBFBD>w<EFBFBD>w member.basic.info # <20>G<EFBFBD>šG<C5A1><47>¦<EFBFBD><C2A6><EFBFBD>T
<EFBFBD>x <20>u<EFBFBD>w<EFBFBD>w member.info.select # GET /api/v1/members/me
<EFBFBD>x <20>u<EFBFBD>w<EFBFBD>w member.info.update # PATCH /api/v1/members/me
<EFBFBD>x <20>|<7C>w<EFBFBD>w member.info.select.plain_code # GET /api/v1/members<72>]<5D><><EFBFBD>X<EFBFBD><58><EFBFBD><EFBFBD><EFBFBD>^
<EFBFBD>u<EFBFBD>w<EFBFBD>w member.admin.list # GET /api/v1/members
<EFBFBD>u<EFBFBD>w<EFBFBD>w member.admin.read # GET /api/v1/members/:uid
<EFBFBD>u<EFBFBD>w<EFBFBD>w member.admin.update # PATCH /api/v1/members/:uid
<EFBFBD>|<7C>w<EFBFBD>w member.admin.status # PATCH /api/v1/members/:uid/status
2026-05-19 13:56:59 +00:00
permission.role.management # <20>@<40>šG<C5A1><47><EFBFBD><EFBFBD><EFBFBD>v<EFBFBD><76><EFBFBD>޲z
<EFBFBD>u<EFBFBD>w<EFBFBD>w permission.role.read # GET /api/v1/permissions/roles
<EFBFBD>u<EFBFBD>w<EFBFBD>w permission.role.write # POST/PUT/DELETE roles
<EFBFBD>u<EFBFBD>w<EFBFBD>w permission.assign.write # POST/DELETE user roles
<EFBFBD>|<7C>w<EFBFBD>w permission.catalog.read # GET /api/v1/permissions/catalog
2026-05-19 13:56:59 +00:00
tenant.management
<EFBFBD>u<EFBFBD>w<EFBFBD>w tenant.read
<EFBFBD>u<EFBFBD>w<EFBFBD>w tenant.ldap.write
<EFBFBD>|<7C>w<EFBFBD>w tenant.sync.trigger
2026-05-19 13:56:59 +00:00
scim.management
<EFBFBD>u<EFBFBD>w<EFBFBD>w scim.users.write
<EFBFBD>|<7C>w<EFBFBD>w scim.groups.write
2026-05-19 13:56:59 +00:00
system.management # <20><><EFBFBD>x<EFBFBD><78>
<EFBFBD>|<7C>w<EFBFBD>w system.tenant.create
2026-05-19 13:56:59 +00:00
```
> **<2A><><EFBFBD><EFBFBD><EFBFBD>`<60>I**<2A>]<5D>L `http_path`<60>^<5E><> UI <20>𪬤Ŀ<F0AAACA4><C4BF>F**<2A><><EFBFBD>`<60>I**<2A>~<7E>g<EFBFBD>J Casbin Policy<63>C
> <EFBFBD>s<EFBFBD>W Permission <20><><EFBFBD><EFBFBD><EFBFBD>x seed migration<6F>F<EFBFBD><46><EFBFBD><EFBFBD>**<2A><><EFBFBD>i**<2A>ۦ<EFBFBD><DBA6>s<EFBFBD>W Permission <20>W<EFBFBD>١C
2026-05-19 13:56:59 +00:00
#### Permission Tree <20><EFBFBD>]<5D>u<EFBFBD><75> permission-server<65>^
2026-05-19 13:56:59 +00:00
1. **`filterOpenNodes`**<2A>G<EFBFBD><47><EFBFBD>`<60>I `status=close` <20><> <20><><EFBFBD>ʤl<CAA4>𤣥i<F0A4A3A5><69>
2. **`getFullParentPermissionIDs`**<2A>G<EFBFBD>Ŀ<EFBFBD><C4BF>l<EFBFBD>v<EFBFBD><76> <20><> <20>۰ʥ[<5B>J<EFBFBD>Ҧ<EFBFBD><D2A6><EFBFBD><EFBFBD>`<60>I ID
3. **`getFullParentPermission`**<2A>G<EFBFBD>d Role <20>v<EFBFBD><76> <20><> <20>^<5E>ǧt<C7A7><74><EFBFBD>`<60>I<EFBFBD><49><EFBFBD><EFBFBD><EFBFBD><EFBFBD> permission name <20><> status map<61>]<5D>ѫe<D1AB><65> UI<55>^
2026-05-19 13:56:59 +00:00
### 6.5 Role Entity<74>]B2B <20><><EFBFBD><EFBFBD><EFBFBD>۩w<DBA9>q<EFBFBD>^
2026-05-19 13:56:59 +00:00
```go
type Role struct {
2026-05-19 17:04:26 +00:00
ID primitive.ObjectID
TenantID string // <20><><EFBFBD><EFBFBD> ID<49>]= <20><> ClientID<49>^
Key string // immutable role key<65>A<EFBFBD><41><EFBFBD><EFBFBD>ߤ@<40>FCasbin enforce <20>Φ<EFBFBD><CEA6><EFBFBD>
DisplayName string // <20><><EFBFBD>ܦW<DCA6>١A<D9A1>i<EFBFBD><69><EFBFBD>W
CreatorUID string // <20>إߪ<D8A5> uid<69>]= <20><> Role.UID<49>A<EFBFBD>i<EFBFBD><69><EFBFBD>^
2026-05-19 17:04:26 +00:00
Status enum.Status // open | close
IsSystem bool // <20>t<EFBFBD><74> seed <20><><EFBFBD>w<EFBFBD>]<5D><><EFBFBD><EFBFBD><EFBFBD>AB2B <20>i<EFBFBD><69> Permission <20><><EFBFBD><EFBFBD><EFBFBD>i<EFBFBD>R<EFBFBD><52> Owner
2026-05-19 17:04:26 +00:00
CreateAt int64
UpdateAt int64
2026-05-19 13:56:59 +00:00
}
2026-05-19 17:04:26 +00:00
// Index: { tenant_id, key } unique
2026-05-19 13:56:59 +00:00
```
> **`Role.Key` <20>W<EFBFBD>d**<2A>G
> - <20><EFBFBD>G`^[a-z][a-z0-9_]{1,63}$`
> - <20><><EFBFBD><EFBFBD>ߤ@<40>F<EFBFBD>إ߫<D8A5>**<2A><><EFBFBD>i<EFBFBD>ק<EFBFBD>**
> - <20>T<EFBFBD><54> `system.` / `platform_` <20>r<EFBFBD><72><EFBFBD>]<5D>O<EFBFBD>d<EFBFBD><64><EFBFBD><EFBFBD><EFBFBD>x<EFBFBD><78> role<6C>^
> - rename <20><> `DisplayName`<60>A<EFBFBD><41><EFBFBD>v<EFBFBD>T UserRole<6C>BRoleMapping<6E>BCasbin policy <20>P<EFBFBD>J<EFBFBD><4A> token
2026-05-19 17:04:26 +00:00
#### B2B <20>۩w<DBA9>q<EFBFBD>W<EFBFBD>h
2026-05-19 13:56:59 +00:00
1. <20><><EFBFBD><EFBFBD><EFBFBD>i **CRUD** <20>ۭq Role<6C>]`is_system=false`<60>^
2. <20>t<EFBFBD><74> seed <20><><EFBFBD>w<EFBFBD>] Role<6C>]`is_system=true`<60>^<5E>i<EFBFBD>ק<EFBFBD> Permission <20><><EFBFBD>X<EFBFBD>A**tenant_owner <20><><EFBFBD>i<EFBFBD>R**
3. Role <20>j<EFBFBD>w<EFBFBD><77> Permission <20><><EFBFBD><EFBFBD><EFBFBD>O<EFBFBD><4F><EFBFBD><EFBFBD> Catalog <20><> `status=open` <20><><EFBFBD>`<60>I
4. <20><><EFBFBD><EFBFBD>**<2A><><EFBFBD>i**<2A>Ŀ<EFBFBD> `system.*` <20>v<EFBFBD><76><EFBFBD>]<5D><><EFBFBD>D<EFBFBD><44><EFBFBD>x<EFBFBD>t<EFBFBD><74><EFBFBD>}<7D>ҡ^
5. <20>ܤ֫O<D6AB>d<EFBFBD>@<40><> Role <20>t `permission.role.write`<EFBFBD>A<EFBFBD>קK<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
2026-05-19 13:56:59 +00:00
#### <20>w<EFBFBD>] Role <20>ҪO<D2AA>]<5D>إ<EFBFBD> B2B tenant <20><> seed<65>^
2026-05-19 13:56:59 +00:00
| Key | DisplayName | <20>w<EFBFBD>]<5D>Ŀ<EFBFBD><C4BF>]Permission Name<6D>^ |
2026-05-19 13:56:59 +00:00
|------|------|----------------------------|
| `tenant_owner` | <20><><EFBFBD><EFBFBD><EFBFBD>֦<EFBFBD><D6A6><EFBFBD> | <20><> `system.*` <20>~<7E><><EFBFBD><EFBFBD> open <20>`<60>I |
| `tenant_admin` | <20><><EFBFBD><EFBFBD><EFBFBD>޲z<DEB2><7A> | member.*, permission.*, tenant.*, scim.* |
| `member_manager` | <20>|<7C><><EFBFBD>޲z | member.admin.list, member.admin.read, member.admin.status |
| `member` | <20>@<40><><EFBFBD>|<7C><> | member.info.select, member.info.update |
| `viewer` | <20><>Ū | member.info.select |
2026-05-19 13:56:59 +00:00
B2B <20>޲z<DEB2><7A><EFBFBD>d<EFBFBD>ҡG
2026-05-19 13:56:59 +00:00
```
<EFBFBD>إ<EFBFBD> Role<6C>Gsales_supervisor
<EFBFBD>Ŀ<EFBFBD><EFBFBD>Gmember.admin.list, member.admin.read
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>GPOST /permissions/users/{uid}/roles { "role_id": "..." }
<EFBFBD><EFBFBD> RolePermission.Create <20><> getFullParentPermissionIDs <20>۰ʸ<DBB0> parent
<EFBFBD><EFBFBD> LoadPolicy <20><><EFBFBD>s Casbin
2026-05-19 13:56:59 +00:00
```
### 6.6 UserRole / RolePermission
```go
type UserRole struct {
TenantID string
UID string
RoleID string // Role._id hex
Source enum.RoleSource // manual | zitadel | ldap | scim
CreateAt int64
UpdateAt int64
}
// Index: { tenant_id, uid, role_id } unique
// Index: { tenant_id, uid }
type RolePermission struct {
2026-05-19 17:04:26 +00:00
TenantID string
2026-05-19 13:56:59 +00:00
RoleID string
PermissionID string
CreateAt int64
UpdateAt int64
}
2026-05-19 17:04:26 +00:00
// Index: { tenant_id, role_id, permission_id } unique
2026-05-19 13:56:59 +00:00
```
> <EFBFBD><EFBFBD> permission-server <20><> UserRole <20><><EFBFBD>@ user <20>@ role<6C>]Update <20>л\<5C>^<5E>F<EFBFBD>s<EFBFBD>]<5D>p**<2A><EFBFBD>h<EFBFBD><68><EFBFBD><EFBFBD>**<2A>AMiddleware <20><><EFBFBD>C<EFBFBD><43> immutable role key <20><> Casbin enforce<63>A<EFBFBD><41><EFBFBD>@ allow <20>Y<EFBFBD>q<EFBFBD>L<EFBFBD>C
2026-05-19 13:56:59 +00:00
### 6.7 Casbin RBAC<41>]<5D>֤߱<D6A4><DFB1>v<EFBFBD><76><EFBFBD><EFBFBD><EFBFBD>^
2026-05-19 13:56:59 +00:00
#### <20>ҫ<EFBFBD><D2AB><EFBFBD> `etc/rbac.conf`<60>]Gateway <20>h<EFBFBD><68><EFBFBD><EFBFBD>^
2026-05-19 13:56:59 +00:00
```ini
[request_definition]
2026-05-19 17:04:26 +00:00
r = tenant, role, path, method
2026-05-19 13:56:59 +00:00
[policy_definition]
2026-05-19 17:04:26 +00:00
p = tenant, role, path, methods, name
2026-05-19 13:56:59 +00:00
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
2026-05-19 17:04:26 +00:00
m = r.tenant == p.tenant && r.role == p.role && keyMatch2(r.path, p.path) && regexMatch(r.method, p.methods)
2026-05-19 13:56:59 +00:00
```
- **`keyMatch2`**<2A>G<EFBFBD>`/api/v1/members/*` <20>U<EFBFBD><55> path
- **`regexMatch`**<2A>G<EFBFBD>`GET|POST` <20>h method <20>g<EFBFBD>b<EFBFBD>P<EFBFBD>@ policy
- **SuperAdmin bypass**<2A>G<EFBFBD><47><EFBFBD><EFBFBD><EFBFBD>b Casbin matcher<65>F<EFBFBD><46> Middleware <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> platform role / allowlist <20><><EFBFBD>u<EFBFBD><75><EFBFBD>A<EFBFBD>üg<C3BC>J audit log
2026-05-19 13:56:59 +00:00
#### Policy <20><><EFBFBD>J<EFBFBD>]`RBACUseCase.LoadPolicy`<60>^
2026-05-19 13:56:59 +00:00
```
1. permissionRepo.GetAll <20><> GeneratePermissionTree <20><> filterOpenNodes
2. roleRepo.All(tenant_id) <20><> <20>C<EFBFBD><43> role <20><> rolePermissionRepo.Get
3. <20><><EFBFBD>C<EFBFBD><43> (role, permission) <20>Y http_path + http_method <20>D<EFBFBD>šG
2026-05-19 17:04:26 +00:00
enforcer.AddPolicy(tenantID, role.Key, permission.HTTPPath, permission.HTTPMethods, permission.Name)
4. adapter.SavePolicy(tenant_id) <20><> Redis List<73>]tenant-scoped casbin rules<65>^
2026-05-19 13:56:59 +00:00
5. enforcer.LoadPolicy()
```
#### <20><><EFBFBD>v<EFBFBD>ˬd<CBAC>]`RBACUseCase.Check`<60>^
2026-05-19 13:56:59 +00:00
```go
// <20><><EFBFBD>J<EFBFBD>GtenantID, roleKey, requestPath, requestMethod
2026-05-19 17:04:26 +00:00
ok, policy, err := enforcer.EnforceEx(tenantID, roleKey, path, method)
2026-05-19 13:56:59 +00:00
// <20>^<5E><> CheckRolePermissionStatus<75>G
2026-05-19 13:56:59 +00:00
// Allow: bool
// PermissionName: string // <20>R<EFBFBD><52><EFBFBD><EFBFBD> permission.Name
// PlainCode: bool // <20>O<EFBFBD>_<EFBFBD><5F> .plain_code <20>l<EFBFBD>v<EFBFBD><76><EFBFBD>]GET <20><><EFBFBD>B<EFBFBD>~<7E>d<EFBFBD>^
2026-05-19 13:56:59 +00:00
```
#### Policy <20>P<EFBFBD>B<EFBFBD><42><EFBFBD><EFBFBD>
2026-05-19 13:56:59 +00:00
| IJ<>o | <20>ʧ@ |
2026-05-19 13:56:59 +00:00
|------|------|
| RolePermission <20>ܧ<EFBFBD> | <20><> tenant `LoadPolicy` + <20>v<EFBFBD><76><EFBFBD>֨<EFBFBD><D6A8><EFBFBD><EFBFBD><EFBFBD> |
| Permission status <20>ܧ<EFBFBD><DCA7>]<5D><><EFBFBD>x<EFBFBD>^ | <20><><EFBFBD><EFBFBD> `LoadAllPolicies` + <20>v<EFBFBD><76><EFBFBD>֨<EFBFBD><D6A8><EFBFBD><EFBFBD><EFBFBD> |
| <20>w<EFBFBD><77> cron<6F>]<5D>p 5min<69>^ | `SyncPolicy` <20>©<EFBFBD> |
| Gateway <20>Ұ<EFBFBD> | <20><><EFBFBD>l `LoadPolicy` |
2026-05-19 13:56:59 +00:00
Redis <20>x<EFBFBD>s Casbin rules<65>G`permission:casbin:rules:{tenant_id}`<60>]List of JSON `rbac.Rule`<EFBFBD>^<5E>C<EFBFBD><43><EFBFBD>q<EFBFBD><71><EFBFBD>J<EFBFBD>ɥi<C9A5><69><EFBFBD>y tenant-scoped keys<79>A<EFBFBD>Υ<EFBFBD> repository <20><> MongoDB role/permission <20><><EFBFBD>ءC
2026-05-19 13:56:59 +00:00
### 6.8 UseCase <20><><EFBFBD><EFBFBD>
2026-05-19 13:56:59 +00:00
```go
// --- Casbin <20><><EFBFBD>v<EFBFBD>]<5D>֤ߡ^---
2026-05-19 13:56:59 +00:00
type RBACUseCase interface {
Check(ctx context.Context, req *CheckRequest) (*CheckResult, error)
LoadPolicy(ctx context.Context, tenantID string) error
LoadAllPolicies(ctx context.Context) error
SyncPolicy(ctx context.Context, interval time.Duration)
}
type CheckRequest struct {
TenantID string
UID string
2026-05-19 17:04:26 +00:00
RoleKey string // immutable Role.Key
Path string // <20><><EFBFBD>ڽШD path
Method string // <20><><EFBFBD><EFBFBD> HTTP method
2026-05-19 13:56:59 +00:00
}
type CheckResult struct {
Allow bool
PermissionName string
PlainCode bool
MatchedRole string
}
// --- Permission Catalog<6F>]<5D><><EFBFBD>x<EFBFBD>š^---
2026-05-19 13:56:59 +00:00
type PermissionUseCase interface {
All(ctx context.Context, status *enum.Status) ([]PermissionDTO, error)
FilterAll(ctx context.Context) ([]PermissionDTO, error) // <20>𪬹L<F0AAACB9>o open <20>`<60>I
Insert(ctx context.Context, req *CreatePermissionRequest) error // <20><><EFBFBD>x Admin
2026-05-19 13:56:59 +00:00
Update(ctx context.Context, id string, req *UpdatePermissionRequest) error
}
// --- Role<6C>]<5D><><EFBFBD><EFBFBD><EFBFBD>šAB2B <20>۩w<DBA9>q<EFBFBD>^---
2026-05-19 13:56:59 +00:00
type RoleUseCase interface {
List(ctx context.Context, req *ListRolesRequest) ([]RoleDTO, int64, error)
All(ctx context.Context, tenantID string) ([]RoleDTO, error)
GetByID(ctx context.Context, tenantID, id string) (*RoleDTO, error)
Create(ctx context.Context, req *CreateRoleRequest) error
Update(ctx context.Context, id string, req *UpdateRoleRequest) error
Delete(ctx context.Context, tenantID, id string) error
}
// --- RolePermission ---
type RolePermissionUseCase interface {
Get(ctx context.Context, tenantID, roleID string) (enum.Permissions, error) // name <20><> open/close
Replace(ctx context.Context, tenantID, roleID string, permNames []string) error // <20><><EFBFBD>q<EFBFBD><71><EFBFBD>N
2026-05-19 13:56:59 +00:00
}
// --- UserRole ---
type UserRoleUseCase interface {
GetByUID(ctx context.Context, tenantID, uid string) ([]UserRoleDTO, error)
GetRoleKeys(ctx context.Context, tenantID, uid string) ([]string, error) // Middleware <20>ΡA<CEA1><41> cache
2026-05-19 17:04:26 +00:00
Assign(ctx context.Context, tenantID, uid, roleID string, source enum.RoleSource) error
2026-05-19 13:56:59 +00:00
Revoke(ctx context.Context, tenantID, uid, roleID string) error
Replace(ctx context.Context, tenantID, uid string, roleIDs []string, source enum.RoleSource) error // <20><><EFBFBD>q<EFBFBD><71><EFBFBD>N<EFBFBD>u<EFBFBD><75> source<63>v<EFBFBD><76><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
2026-05-19 13:56:59 +00:00
}
// --- <20>~<7E><><EFBFBD>M<EFBFBD>g ---
2026-05-19 13:56:59 +00:00
type RoleMappingUseCase interface {
List(ctx context.Context, tenantID string) ([]RoleMappingDTO, error)
Upsert(ctx context.Context, req *UpsertRoleMappingRequest) error
Delete(ctx context.Context, tenantID, id string) error
SyncFromZitadelClaims(ctx context.Context, req *SyncFromZitadelRequest) error
SyncFromScimGroup(ctx context.Context, req *SyncFromScimGroupRequest) error
SyncFromLDAPGroups(ctx context.Context, req *SyncFromLDAPGroupsRequest) error
}
// --- <20>E<EFBFBD>X<EFBFBD>d<EFBFBD>ߡ]<5D>e<EFBFBD>ݵ<EFBFBD>?<3F>^---
2026-05-19 13:56:59 +00:00
type AuthorizationQueryUseCase interface {
GetMyPermissions(ctx context.Context, tenantID, uid string) (enum.Permissions, error)
GetMyRoles(ctx context.Context, tenantID, uid string) ([]string, error)
}
```
> **<2A>󯲤ᨾ<F3AFB2A4>b**<2A>G<EFBFBD>Ҧ<EFBFBD> mutation usecase<73>]Role*, RolePermission*, UserRole*, RoleMapping*<2A>^<5E>i<EFBFBD>J<EFBFBD>ɥ<EFBFBD><C9A5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> target ID <20>ݩ<EFBFBD> `tenantID`<60>Frepository <20>d<EFBFBD>ߤ@<40>߱a `{tenant_id, _id}`<60>A<EFBFBD><EFBFBD><E4A4A3><EFBFBD>^ `ErrRoleNotInTenant` / `ErrUserRoleNotInTenant`<60>C
> Logic <20>h**<2A>T<EFBFBD><54>**<2A><> path <20><> `:id` <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> usecase <20>Ӥ<EFBFBD><D3A4>a `tenant_id`<60>C
2026-05-19 17:04:26 +00:00
### 6.9 Middleware <20><><EFBFBD>v<EFBFBD>y<EFBFBD>{
2026-05-19 13:56:59 +00:00
```
Request<EFBFBD>]JwtRevokeMiddleware <20>w<EFBFBD><77><EFBFBD>L JWT + auth_gen<65>^
1. <20><> ctx.tenant_id, ctx.uid
2. userRoleUC.GetRoleKeys <20><> []Role.Key<65>]<5D><> perm:user_roles cache<68>^
3. <20><><EFBFBD>C<EFBFBD><43> roleKey enforce(tenantID, roleKey, path, method)<29>F
<20>E<EFBFBD>X<EFBFBD>Ҧ<EFBFBD> allow <20><><EFBFBD>G<EFBFBD><47> []CheckResult
4. <20>Y<EFBFBD>L<EFBFBD><4C><EFBFBD><EFBFBD> allow <20><> 403 Forbidden
5. <20>E<EFBFBD>X<EFBFBD>W<EFBFBD>h<EFBFBD>G
- PermissionNames = <20>Ҧ<EFBFBD> allow <20>R<EFBFBD><52><EFBFBD><EFBFBD> permission.Name<6D>]<5D>h<EFBFBD><68><EFBFBD>^
- PlainCode = <20><><EFBFBD>C<EFBFBD>өR<D3A9><52> permission<6F>A<EFBFBD>B<EFBFBD>~ enforce
(permission.Name + ".plain_code") <20><><EFBFBD><EFBFBD><EFBFBD>F<EFBFBD><46><EFBFBD>@<40>q<EFBFBD>L <20><> true
6. <20>`<60>J ctx.permission_names, ctx.plain_code
2026-05-19 13:56:59 +00:00
```
> **PlainCode <20><><EFBFBD>@**<2A>G`*.plain_code` <20>P<EFBFBD>@<40><> leaf <20>@<40>˼g<CBBC>J Casbin policy<63>FCheck <20>ɥD permission <20>R<EFBFBD><52><EFBFBD><EFBFBD><EFBFBD>A<EFBFBD>ΦP<CEA6>@ `(tenantID, roleKey, path, method)` <20>A<EFBFBD><41><EFBFBD>@<40><><EFBFBD>a `.plain_code` <20><> EnforceEx<45>C<EFBFBD>S<EFBFBD><53> plain_code <20><><EFBFBD><EFBFBD> <20><> false<73>C
> Logic <20>hŪ `ctx.plain_code` <20>M<EFBFBD>w<EFBFBD>O<EFBFBD>_<EFBFBD>^<5E>ǩ<EFBFBD><C7A9>X<EFBFBD><58><EFBFBD><EFBFBD><EFBFBD>C
2026-05-19 17:04:26 +00:00
> **Platform Admin bypass** <20><> `JwtRevokeMiddleware` <20><> 0 <20>B<EFBFBD>B<EFBFBD>z<EFBFBD>]<5D><> <20><>4.6<EFBFBD>^<5E>A<EFBFBD><41><EFBFBD>i<EFBFBD>o<EFBFBD>Ӭy<D3AC>{<7B>C
2026-05-19 13:56:59 +00:00
### 6.10 <20>~<7E><> Group / Role <20>M<EFBFBD>g
2026-05-19 13:56:59 +00:00
```go
type RoleMapping struct {
TenantID string
ExternalSource enum.RoleSource // zitadel | ldap | scim
ExternalKey string // ZITADEL role / LDAP group DN / SCIM group id
InternalRoleID string // <20><><EFBFBD><EFBFBD> Role._id hex
InternalRoleKey string // denormalized Role.Key<65>A<EFBFBD><41><EFBFBD>K<EFBFBD>d<EFBFBD>߻P<DFBB>f<EFBFBD>p
2026-05-19 13:56:59 +00:00
}
// Index: { tenant_id, external_source, external_key } unique
```
| <20>ӷ<EFBFBD> | ExternalKey <20>d<EFBFBD><64> | <20>M<EFBFBD>g<EFBFBD><67> |
2026-05-19 13:56:59 +00:00
|------|------------------|--------|
| ZITADEL | `org_admin` | `tenant_admin` |
| LDAP (AD) | `CN=CloudEP-Admins,OU=Groups,DC=acme,DC=com` | <20><><EFBFBD><EFBFBD><EFBFBD>ۭq Role.Key |
| LDAP (OpenLDAP) | `cn=admins,ou=groups,dc=acme,dc=com` | <20><><EFBFBD><EFBFBD><EFBFBD>ۭq Role.Key |
| SCIM Group | `group-uuid-xxx` | <20><><EFBFBD><EFBFBD><EFBFBD>ۭq Role.Key |
2026-05-19 13:56:59 +00:00
<EFBFBD><EFBFBD> B2B <20><><EFBFBD><EFBFBD><EFBFBD>޲z<DEB2><7A><EFBFBD>b<EFBFBD><62><EFBFBD>x<EFBFBD>]<5D>w<EFBFBD>]<5D>ݩR<DDA9><52> `permission.role.write` <20><><EFBFBD><EFBFBD> API<50>^<5E>C
2026-05-19 13:56:59 +00:00
#### <20>~<7E><><EFBFBD>ӷ<EFBFBD><D3B7>P<EFBFBD>B<EFBFBD>W<EFBFBD>h<EFBFBD>]<5D>קK<D7A7>~<7E><> manual <20><><EFBFBD><EFBFBD><EFBFBD>^
2026-05-19 17:04:26 +00:00
`SyncFromZitadelClaims` / `SyncFromScimGroup` / `SyncFromLDAPGroups` <20>@<40>ߥH **`source` <20><><EFBFBD><EFBFBD>**<2A><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>q<EFBFBD><71><EFBFBD>N<EFBFBD>G
2026-05-19 17:04:26 +00:00
```
UserRoleUC.Replace(tenantID, uid, roleIDs, source = zitadel)
<20><> DELETE user_roles WHERE tenant_id=? AND uid=? AND source='zitadel'
<20><> INSERT <20>s<EFBFBD><73> roleIDs<44>]source='zitadel'<27>^
<20><> source='manual' / 'scim' / 'ldap' <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>v<EFBFBD>T
2026-05-19 17:04:26 +00:00
```
> <EFBFBD><EFBFBD><EFBFBD>ӷ<EFBFBD><EFBFBD>Ĭ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>h<EFBFBD>GUserRole <20><><EFBFBD>u<EFBFBD>ö<EFBFBD><C3B6>v<EFBFBD>A<EFBFBD><41><EFBFBD>@ source <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> role <20>Y<EFBFBD>ͮġFrevoke <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>w source<63>C
2026-05-19 17:04:26 +00:00
### 6.11 <20>v<EFBFBD><76><EFBFBD>ܧ<EFBFBD><DCA7>ͮ<EFBFBD>
2026-05-19 13:56:59 +00:00
| <20>ƥ<EFBFBD> | <20>ʧ@ |
2026-05-19 13:56:59 +00:00
|------|------|
| RolePermission Create/Delete | `LoadPolicy(tenant_id)` + `perm:role_perms:*` <20>֨<EFBFBD><D6A8><EFBFBD><EFBFBD><EFBFBD> |
2026-05-19 13:56:59 +00:00
| Role Create/Update/Delete | `LoadPolicy(tenant_id)` |
2026-05-19 17:04:26 +00:00
| UserRole Assign/Revoke | **`INCR auth:gen`** + `LoadPolicy(tenant_id)` |
| SCIM / LDAP Group <20>ܧ<EFBFBD> | <20><><EFBFBD>s user_roles <20><> `LoadPolicy` + **`INCR auth_gen`** |
| Permission status <20>ܧ<EFBFBD><DCA7>]<5D><><EFBFBD>x<EFBFBD>^ | `LoadAllPolicies()` + <20>v<EFBFBD><76><EFBFBD>֨<EFBFBD><D6A8><EFBFBD><EFBFBD>ġF<C4A1>Y<EFBFBD>ܧ<EFBFBD><DCA7>v<EFBFBD>T<EFBFBD>n<EFBFBD>J<EFBFBD><4A><EFBFBD>A<EFBFBD>A batch `INCR auth_gen` |
2026-05-19 17:04:26 +00:00
#### <20>h Pod <20>P<EFBFBD>B<EFBFBD><42><EFBFBD><EFBFBD><EFBFBD>]<5D>w<EFBFBD>M<EFBFBD><4D><EFBFBD>^
2026-05-19 17:04:26 +00:00
```
Channel: casbin:reload
Payload: { "tenant_id": "xxx", "ts": 1716120000000 } # tenant_id == "*" <20>N<EFBFBD><4E><EFBFBD><EFBFBD><EFBFBD>q
2026-05-19 17:04:26 +00:00
```
- **<2A>Y<EFBFBD>ɳq<C9B3>D**<2A>GPub/Sub
- Writer<65>G<EFBFBD>C<EFBFBD><43> `LoadPolicy(tenant_id)` <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> `PUBLISH casbin:reload {tenant_id}`
- Subscriber<65>G<EFBFBD>C<EFBFBD><43> pod <20>Ұʮ<D2B0> `SUBSCRIBE`<EFBFBD>F<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>b<EFBFBD>O<EFBFBD><EFBFBD><EFBFBD>餤 reload <20><><EFBFBD><EFBFBD> tenant <20><> policy
- **<2A>©<EFBFBD>**<2A>G<EFBFBD>C pod <20>ҰʱƵ{ `5min` <20><><EFBFBD>q `LoadAllPolicies()`<EFBFBD>F<EFBFBD><EFBFBD> pub message <20>|<7C><><EFBFBD>]pod <20>Ұ<EFBFBD><D2B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD>BRedis <20>s<EFBFBD>u<EFBFBD>ݰʡ^
- **<2A><><EFBFBD><EFBFBD>**<2A>Greload <20>γ<EFBFBD><CEB3>@ mutex per tenant<6E>F<EFBFBD>P<EFBFBD>ɬq<C9AC>h<EFBFBD><68> message <20><75>o<EFBFBD>@<40><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> IO
- **<2A><><EFBFBD><EFBFBD><EFBFBD>Ұ<EFBFBD>**<2A>Gpod <20>Ұʥ<D2B0><CAA5><EFBFBD><EFBFBD>@<40><> `LoadAllPolicies()`<EFBFBD>A<EFBFBD>A<EFBFBD>}<7D>l SUBSCRIBE
- <20>]<5D>w<EFBFBD>G`Permission.PolicySyncInterval: 5m`<60>B`Permission.PolicyReloadChannel: casbin:reload`
2026-05-19 17:04:26 +00:00
### 6.12 B2C vs B2B <20>v<EFBFBD><76><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]<5D>w<EFBFBD>M<EFBFBD><4D><EFBFBD>^
2026-05-19 13:56:59 +00:00
| <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> | Role <20>۩w<DBA9>q | Permission <20>Ŀ<EFBFBD> | API <20><><EFBFBD><EFBFBD> |
2026-05-19 17:04:26 +00:00
|----------|-------------|-----------------|----------|
| **B2C** | **<EFBFBD><EFBFBD><EFBFBD>i**<2A>]<5D><>Ū seed <20>ҪO<D2AA>^ | <20>T<EFBFBD>w<EFBFBD>A<EFBFBD><41><EFBFBD>i<EFBFBD><69> | <20>T<EFBFBD><54> `POST/PUT/DELETE /permissions/roles*` |
| **B2B** | **<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>۩w<EFBFBD>q** | <20>q<EFBFBD><71><EFBFBD><EFBFBD> Catalog <20>ۥѤĿ<D1A4> | <20><><EFBFBD><EFBFBD> permission API |
| **Hybrid** | <20><> tenant.type <20><><EFBFBD><EFBFBD><EFBFBD>P<EFBFBD>_ | B2B <20>q<EFBFBD>i<EFBFBD>۩w<DBA9>q | middleware <20>ˬd tenant <20><><EFBFBD><EFBFBD> |
2026-05-19 13:56:59 +00:00
B2C <20><><EFBFBD><EFBFBD><EFBFBD>إ߮ɥu seed <20>T<EFBFBD>w Role<6C>]<5D>p `member`<EFBFBD>B`viewer`<60>^<5E>A**<2A><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>** Role CRUD <20>P Permission <20>Ŀ<EFBFBD> API<50>]Casbin <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>J seed <20><><EFBFBD>G<EFBFBD>^<5E>C
2026-05-19 13:56:59 +00:00
---
## 7. API <20>W<EFBFBD><57>
2026-05-19 13:56:59 +00:00
<EFBFBD>ɮסG`generate/api/`
2026-05-19 13:56:59 +00:00
### 7.1 auth.api<70>]<5D><><EFBFBD>} / <20><> JWT <20><> API <20>өw<D3A9>^
2026-05-19 13:56:59 +00:00
| Method | Path | <20><><EFBFBD><EFBFBD> | Ų<>v |
2026-05-19 17:04:26 +00:00
|--------|------|------|------|
| POST | `/api/v1/auth/token/exchange` | ZITADEL token <20><> CloudEP JWT | <20><><EFBFBD>} |
| POST | `/api/v1/auth/token/refresh` | <20><><EFBFBD>s JWT | <20><><EFBFBD>}<7D>]<5D>a refresh<73>^ |
| POST | `/api/v1/auth/logout` | <20>n<EFBFBD>X<EFBFBD>]jti <20>¦W<C2A6><57><EFBFBD>^ | JWT |
| POST | `/api/v1/auth/revoke-all` | <20>M<EFBFBD>P<EFBFBD>ۤv<DBA4>Ҧ<EFBFBD> session<6F>]INCR auth_gen<65>^ | JWT + Step-up `revoke_all_sessions` |
| POST | `/api/v1/auth/step-up/start` | <20>Ұ<EFBFBD> step-up MFA<46>A<EFBFBD>H OTP | JWT |
| POST | `/api/v1/auth/step-up/confirm` | <20>T<EFBFBD>{ OTP <20><> ñ<>o<EFBFBD>u<EFBFBD><75> `step_up_token` | JWT |
2026-05-19 13:56:59 +00:00
### 7.2 member.api<70>]<5D><> JWT + Casbin<69>^
2026-05-19 13:56:59 +00:00
| Method | Path | Casbin <20>R<EFBFBD><52> Permission<6F>]<5D>ܨҡ^ | Step-up |
2026-05-19 17:04:26 +00:00
|--------|------|-------------------------------|---------|
| GET | `/api/v1/members/me` | `member.info.select` | <20>X |
| PATCH | `/api/v1/members/me` | `member.info.update` | <20>X |
| PATCH | `/api/v1/members/me/business-email` | `member.info.update` | ? `change_business_email` |
| PATCH | `/api/v1/members/me/business-phone` | `member.info.update` | ? `change_business_phone` |
| DELETE | `/api/v1/members/me` | `member.info.delete` | ? `delete_member` |
| POST | `/api/v1/members/me/verifications/email/start` | `member.info.update` | <20>X |
| POST | `/api/v1/members/me/verifications/email/confirm` | `member.info.update` | <20>X |
| POST | `/api/v1/members/me/verifications/phone/start` | `member.info.update` | <20>X |
| POST | `/api/v1/members/me/verifications/phone/confirm` | `member.info.update` | <20>X |
| GET | `/api/v1/members/me/totp` | `member.info.select` | <20>X |
| POST | `/api/v1/members/me/totp/enroll-start` | `member.info.update` | <20>X |
| POST | `/api/v1/members/me/totp/enroll-confirm` | `member.info.update` | <20>X |
| POST | `/api/v1/members/me/totp/backup-codes` | `member.info.update` | ? `disable_totp` |
| DELETE | `/api/v1/members/me/totp` | `member.info.update` | ? `disable_totp` |
| GET | `/api/v1/members` | `member.admin.list` | <20>X |
| GET | `/api/v1/members/:uid` | `member.admin.read` | <20>X |
| PATCH | `/api/v1/members/:uid` | `member.admin.update` | <20>X |
| PATCH | `/api/v1/members/:uid/status` | `member.admin.status` | ? `tenant_admin_force_status` |
> <EFBFBD><EFBFBD><EFBFBD>v<EFBFBD><EFBFBD> **Casbin <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> path + method** <20>M<EFBFBD>w<EFBFBD>A<EFBFBD>D<EFBFBD>w<EFBFBD>s<EFBFBD>X permission <20>r<EFBFBD><72><EFBFBD>C
> Step-up <20>欰?<3F>̻ݦb Header <20>a `X-Step-Up-Token`<60>A<EFBFBD>B token claim <20><> `action` <20><><EFBFBD><EFBFBD><EFBFBD>P<EFBFBD><50><EFBFBD>C action <20>@<40>P<EFBFBD>]<5D><> <20><>5.6<EFBFBD>^<5E>C
### 7.3 permission.api<70>]<5D><> JWT + Casbin<69>^
| Method | Path | <20><><EFBFBD><EFBFBD> |
2026-05-19 13:56:59 +00:00
|--------|------|------|
| GET | `/api/v1/permissions/catalog` | <20><><EFBFBD><EFBFBD> Permission Tree<65>]open <20>`<60>I<EFBFBD>^ |
| GET | `/api/v1/permissions/me` | <20><><EFBFBD>e<EFBFBD>ϥΪ̪<CEAA> permission name <20><> status map |
| GET | `/api/v1/permissions/roles` | <20>C<EFBFBD>X<EFBFBD><58><EFBFBD><EFBFBD> Role |
| POST | `/api/v1/permissions/roles` | <20>إ<EFBFBD> Role<6C>]B2B<32>^ |
| PUT | `/api/v1/permissions/roles/:id` | <20><><EFBFBD>s Role |
| DELETE | `/api/v1/permissions/roles/:id` | <20>R<EFBFBD><52> Role |
| GET | `/api/v1/permissions/roles/:id/permissions` | <20><><EFBFBD>o Role <20>Ŀ諸 Permission |
| PUT | `/api/v1/permissions/roles/:id/permissions` | <20><><EFBFBD>q<EFBFBD><71><EFBFBD>N Role <20>Ŀ<EFBFBD> `{ "permission_names": [...] }`<EFBFBD>]PermissionTree <20><><EFBFBD><EFBFBD> + <20><> parent<6E>^ |
| GET | `/api/v1/permissions/users/:uid/roles` | <20>d<EFBFBD>ϥΪ̨<CEAA><CCA8><EFBFBD> |
| POST | `/api/v1/permissions/users/:uid/roles` | <20><><EFBFBD><EFBFBD> Role `{ "role_id": "..." }` |
| DELETE | `/api/v1/permissions/users/:uid/roles/:role_id` | <20>M<EFBFBD>P Role |
| GET | `/api/v1/permissions/role-mappings` | <20>~<7E><> Group <20>M<EFBFBD>g<EFBFBD>C<EFBFBD><43> |
| PUT | `/api/v1/permissions/role-mappings` | <20>s<EFBFBD>W/<2F><><EFBFBD>s<EFBFBD>M<EFBFBD>g |
| POST | `/api/v1/permissions/policy/reload` | <20><><EFBFBD><EFBFBD>IJ<EFBFBD>o LoadPolicy<63>]<5D><><EFBFBD>x Admin<69>^ |
### 7.4 tenant.api<70>]<5D><><EFBFBD>x / <20><><EFBFBD><EFBFBD> Admin<69>^
| Method | Path | Casbin <20>R<EFBFBD><52> Permission<6F>]<5D>ܨҡ^ |
2026-05-19 13:56:59 +00:00
|--------|------|-------------------------------|
| POST | `/api/v1/admin/tenants` | `system.tenant.create` |
| GET | `/api/v1/admin/tenants/:tenant_id` | `tenant.read` |
| PUT | `/api/v1/admin/tenants/:tenant_id/ldap` | `tenant.ldap.write` |
| POST | `/api/v1/admin/tenants/:tenant_id/directory-sync` | `tenant.sync.trigger` |
### 7.5 scim.api<70>]SCIM Bearer Token<65>A<EFBFBD>D JWT<57>^
2026-05-19 13:56:59 +00:00
**<2A>w<EFBFBD>M<EFBFBD><4D><EFBFBD><EFBFBD><EFBFBD><EFBFBD>**<2A>G<EFBFBD>H **`tenant_id`** <20><> path <20>Ѽơ]<5D><><EFBFBD>Τl<CEA4><6C><EFBFBD>W<EFBFBD>^
2026-05-19 17:04:26 +00:00
2026-05-19 13:56:59 +00:00
```
/scim/v2/tenants/{tenant_id}/Users
/scim/v2/tenants/{tenant_id}/Groups
2026-05-19 17:04:26 +00:00
/scim/v2/tenants/{tenant_id}/ServiceProviderConfig
/scim/v2/tenants/{tenant_id}/Schemas
2026-05-19 13:56:59 +00:00
```
<EFBFBD>{<7B>ҡG`Authorization: Bearer {tenant_scim_token}`<60>]hash <20>s<EFBFBD><73> tenant <20>]<5D>w<EFBFBD>^
2026-05-19 13:56:59 +00:00
- `{tenant_id}` = ZITADEL `org_id`<EFBFBD>A<EFBFBD>P JWT `tenant_id` <20>@<40>P
- SCIM <20>ШD<D0A8><44><EFBFBD><EFBFBD> CloudEP JWT<57>F<EFBFBD><46><EFBFBD>v<EFBFBD><76> tenant <20><> SCIM token + <20>i<EFBFBD><69> Casbin <20>Ӥ<EFBFBD>
2026-05-19 13:56:59 +00:00
---
## 8. Middleware <20><>
2026-05-19 13:56:59 +00:00
### 8.1 <20>@<40><><EFBFBD><EFBFBD><EFBFBD>O<EFBFBD>@ API
2026-05-19 13:56:59 +00:00
```
Request
<20><> go-zero JWT <20><>ñ
<20><> JwtRevokeMiddleware<72>]jti <20>¦W<C2A6><57> + auth_gen<65>^
<20><> TenantContextMiddleware<72>]<5D><><EFBFBD><EFBFBD> tenant_id <20>@<40>P<EFBFBD>^
<20><> CasbinRBACMiddleware<72>]tenant_id <20><> role_key <20><> path <20><> method <20><> Allow<6F>^
<20><> handler <20><> logic <20><> usecase
2026-05-19 13:56:59 +00:00
```
### 8.2 CasbinRBACMiddleware
> Platform Admin bypass <20>b<EFBFBD>e<EFBFBD>@<40>h `JwtRevokeMiddleware` <20><> 0 <20>B<EFBFBD>B<EFBFBD>z<EFBFBD>]<5D><>4.6<EFBFBD>^<5E>A<EFBFBD><41><EFBFBD>B<EFBFBD><42><EFBFBD><EFBFBD><EFBFBD>ơC
2026-05-19 17:04:26 +00:00
2026-05-19 13:56:59 +00:00
```go
// ?<3F>N?
2026-05-19 17:04:26 +00:00
roleKeys, _ := userRoleUC.GetRoleKeys(ctx, tenantID, uid)
var hits []rbac.CheckResult
for _, roleKey := range roleKeys {
res, _ := rbacUC.Check(ctx, &rbac.CheckRequest{
2026-05-19 13:56:59 +00:00
TenantID: tenantID, UID: uid,
2026-05-19 17:04:26 +00:00
RoleKey: roleKey, Path: r.URL.Path, Method: r.Method,
2026-05-19 13:56:59 +00:00
})
2026-05-19 17:04:26 +00:00
if res.Allow {
hits = append(hits, res)
2026-05-19 13:56:59 +00:00
}
}
2026-05-19 17:04:26 +00:00
if len(hits) == 0 {
httpx.Error(w, forbidden)
return
}
names, plain := aggregate(hits) // <20>h<EFBFBD><68> + PlainCode OR
2026-05-19 17:04:26 +00:00
ctx = withPermissionNames(ctx, names)
ctx = withPlainCode(ctx, plain)
next(w, r)
2026-05-19 13:56:59 +00:00
```
### 8.3 SCIM API
```
Request
<20><> ScimAuthMiddleware<72>]tenant_scim_token<65>^
<20><> TenantContextMiddleware
<20><> handler
2026-05-19 13:56:59 +00:00
```
### 8.4 Logic <20>h<EFBFBD>ɥR<C9A5><52><EFBFBD>v
2026-05-19 13:56:59 +00:00
Casbin <20>B<EFBFBD>z **API <20><>** <20><><EFBFBD>v<EFBFBD>CLogic <20><><EFBFBD>i<EFBFBD>l<EFBFBD>[ **<EFBFBD><EFBFBD><EFBFBD>** <20>P<EFBFBD>_<EFBFBD>G
2026-05-19 13:56:59 +00:00
- `member.info.select` vs <20>d<EFBFBD>L<EFBFBD>H<EFBFBD>G<EFBFBD>Y path <20>t `:uid` <20>B uid <20><> caller<65>A<EFBFBD>ݩR<DDA9><52> `member.admin.read`
- `PlainCode`<EFBFBD>GLogic Ū `ctx.plain_code`<EFBFBD>A<EFBFBD>M<EFBFBD>w<EFBFBD>O<EFBFBD>_<EFBFBD>^<5E>ǩ<EFBFBD><C7A9>X<EFBFBD><58><EFBFBD><EFBFBD>
- **Step-up <20>u<EFBFBD><75>**<2A>]<5D><><EFBFBD><EFBFBD><EFBFBD>I action<6F>^<5E>G
1. <20>q Header `X-Step-Up-Token` <20><> token
2026-05-19 17:04:26 +00:00
2. `auth.StepUpTokenUseCase.Verify(token, expectedAction, tenantID, uid)`
- <20><> `typ == "step_up"`<EFBFBD>B`action == expectedAction`<60>B`tenant_id` / `uid` <20>P ctx <20>@<40>P<EFBFBD>B<EFBFBD><42><EFBFBD>L<EFBFBD><4C>
3. `SETNX auth:stepup:used:{jti}=1`<EFBFBD>A<EFBFBD>w<EFBFBD>s<EFBFBD>b <20><> `403 step_up_replay`
4. <20><><EFBFBD><EFBFBD><EFBFBD>q<EFBFBD>L <20><> <20><><EFBFBD><EFBFBD><EFBFBD>~<7E>Ⱦާ@
5. <20><><EFBFBD><EFBFBD> <20><> `403 step_up_required` + `{ required_action: "<action>" }`
2026-05-19 13:56:59 +00:00
---
## 9. <20>֤߬y<DFAC>{
2026-05-19 13:56:59 +00:00
### 9.1 <20>n<EFBFBD>J / <20><><EFBFBD><EFBFBD>
2026-05-19 13:56:59 +00:00
```
Client <20><> ZITADEL OIDC Login<69>]<5D>t LDAP IdP<64>^
Client <20><> POST /auth/token/exchange { tenant_slug, id_token }
2026-05-19 13:56:59 +00:00
1. zitadel.VerifyIDToken
2. tenant.ResolveBySlug <20><> <20><><EFBFBD><EFBFBD> org_id
3. member.EnsureFromOIDC <20><> uid<69>]<5D>p AMEX-10000000<30>^
4. permission.SyncFromZitadelClaims <20><> user_roles
5. auth.IssueTokenPair<69>]role keys <20>ַ<EFBFBD>, auth_gen<65>^
Client <20><> { access_token, refresh_token, uid }
2026-05-19 13:56:59 +00:00
```
### 9.2 <20><><EFBFBD>O<EFBFBD>@ API
2026-05-19 13:56:59 +00:00
```
Client <20><> GET /api/v1/members/me (Bearer access_jwt)
1. JWT + <20>¦W<C2A6><57> + auth_gen
2. CasbinRBACMiddleware <20><> Check(role, "/api/v1/members/me", "GET")
2026-05-19 13:56:59 +00:00
3. member.GetByUID
```
### 9.3 B2B <20>۩w<DBA9>q Role + <20>Ŀ<EFBFBD> Permission
2026-05-19 13:56:59 +00:00
```
Tenant Admin <20><> PUT /api/v1/permissions/roles/{id}/permissions
2026-05-19 17:04:26 +00:00
{ "permission_names": ["member.admin.list", "member.admin.read"] }
<20><> RolePermissionUC.Replace<63>]<5D><><EFBFBD>q<EFBFBD><71><EFBFBD>N<EFBFBD>^
<20><> PermissionTree.getFullParentPermissionIDs<44>]<5D>۰ʸ<DBB0> parent<6E>^
<20><> RBACUC.LoadPolicy(tenant_id) + <20>s<EFBFBD><73> reload<61>]<5D><> <20><>6.11<EFBFBD>^
2026-05-19 13:56:59 +00:00
Tenant Admin <20><> POST /api/v1/permissions/users/{uid}/roles
2026-05-19 13:56:59 +00:00
{ "role_id": "..." }
<20><> UserRoleUC.Assign(tenantID, uid, roleID, source=manual)
<20><> INCR auth_gen + DEL perm:user_roles cache
2026-05-19 13:56:59 +00:00
```
### 9.4 <20><><EFBFBD>v
2026-05-19 13:56:59 +00:00
```
Admin <20><> PATCH /api/v1/members/:uid/status { status: "suspended" }
2026-05-19 17:04:26 +00:00
Header: X-Step-Up-Token: <step_up_token, action=tenant_admin_force_status>
1. Casbin enforce <20>R<EFBFBD><52> member.admin.status
2. Logic <20><> step_up_token + action <20>@<40>P
2026-05-19 17:04:26 +00:00
3. member.UpdateStatus
4. auth.RevokeAllForUser<65>]INCR auth:gen:{tenant_id}:{uid}<7D>^
2026-05-19 17:04:26 +00:00
```
### 9.5 <20>~<7E><> Email <20><><EFBFBD><EFBFBD>
2026-05-19 17:04:26 +00:00
```
Client <20><> POST /api/v1/members/me/verifications/email/start { target: "biz@foo.com" }
1. (<28>i<EFBFBD><69>) <20>ˬd target <20><><EFBFBD>Q<EFBFBD>P<EFBFBD><50><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>L member <20>ϥ<EFBFBD>
2. <20>ˬd verify:rate:{tenant}:{uid}:email <20><><EFBFBD>s<EFBFBD>b<EFBFBD>]60s <20>N<EFBFBD>o<EFBFBD>^
3. <20>ͦ<EFBFBD> 6 <20>X OTP <20><> bcrypt <20>s verify:otp:{tenant}:{uid}:email:{challenge_id} TTL 5min
2026-05-19 17:04:26 +00:00
4. NotificationClient.Email.Send(target, template=VerifyEmail, data={code})
5. SETEX verify:rate:{tenant}:{uid}:email 60
6. audit log
Client <20><> { challenge_id, expires_in: 300 }
2026-05-19 17:04:26 +00:00
Client <20><> POST /api/v1/members/me/verifications/email/confirm { challenge_id, code }
1. Ū challenge<67>F<EFBFBD>L<EFBFBD><4C><EFBFBD>Υ<EFBFBD><CEA5><EFBFBD> 5 <20><> <20><> <20>ڵ<EFBFBD>
2. bcrypt compare<72>F<EFBFBD><46><EFBFBD><EFBFBD> <20><> INCR AttemptCnt <20><> <20>ڵ<EFBFBD>
3. <20><><EFBFBD>\ <20><> member.BusinessEmail = target, BusinessEmailVerified = true, BusinessEmailVerifiedAt = now
2026-05-19 17:04:26 +00:00
4. DEL challenge
5. audit log
Client <20><> { verified: true }
2026-05-19 17:04:26 +00:00
```
> phone <20>y<EFBFBD>{<7B>P<EFBFBD>W<EFBFBD>AOTP <20>q<EFBFBD>D<EFBFBD><44> SMS Provider<65>Ftemplate <20><> `VerifyPhone`<60>C
2026-05-19 17:04:26 +00:00
### 9.6 Step-up MFA + <20><><EFBFBD>~<7E><> Email
2026-05-19 17:04:26 +00:00
```
Client <20><> POST /api/v1/auth/step-up/start { action: "change_business_email" }
1. <20>q ctx.uid Ū member<65>F<EFBFBD>n<EFBFBD>D BusinessEmailVerified || BusinessPhoneVerified
2. <20><><EFBFBD>q<EFBFBD>D<EFBFBD>G<EFBFBD>u<EFBFBD><75> phone<6E>]<5D>p<EFBFBD>w verified<65>^<5E>_<EFBFBD>h email
3. <20>ͦ<EFBFBD> OTP <20><> <20>H<EFBFBD>X<EFBFBD>]<5D>B<EFBFBD>J<EFBFBD>P <20><>9.5<EFBFBD>^
Client <20><> { challenge_id, channel, expires_in: 300 }
2026-05-19 17:04:26 +00:00
Client <20><> POST /api/v1/auth/step-up/confirm { challenge_id, code, action: "change_business_email" }
1. bcrypt <20><><EFBFBD><EFBFBD><EFBFBD>F<EFBFBD><46> challenge.kind == step_up && target == action
2. auth.StepUpTokenUseCase.Issue(tenant, uid, action) <20><> JWT (typ=step_up, action, TTL 5min)
Client <20><> { step_up_token, token_type: "step_up", expires_in: 300 }
2026-05-19 17:04:26 +00:00
Client <20><> PATCH /api/v1/members/me/business-email { new_email: "new@foo.com" }
2026-05-19 17:04:26 +00:00
Header: X-Step-Up-Token: <step_up_token>
1. Middleware <20>q<EFBFBD>L<EFBFBD>]<5D>@<40><> JWT + Casbin<69>^
2. Logic step-up <20>u<EFBFBD><75><EFBFBD>]<5D><> <20><>8.4<EFBFBD>^
3. <20><><EFBFBD>] BusinessEmailVerified = false<73>ABusinessEmail = new_email
4. <20><><EFBFBD><EFBFBD>IJ<EFBFBD>o <20><>9.5 <20><> new_email <20><><EFBFBD>s<EFBFBD>o OTP<54>]<5D>Ϊ<EFBFBD><CEAA><EFBFBD><EFBFBD>^ challenge_id <20><><EFBFBD>e<EFBFBD>ݡ^
5. audit log<6F>]<5D>t<EFBFBD><74> / <20>s email<69>Bstep_up jti<74>BIP<49>BUA<55>^
2026-05-19 13:56:59 +00:00
```
---
## 10. LDAP <20>P SCIM
2026-05-19 13:56:59 +00:00
### 10.1 <20>T<EFBFBD><54> Provisioning <20><><EFBFBD>|
2026-05-19 13:56:59 +00:00
| <20><><EFBFBD>| | <20>A<EFBFBD><41> | <20><><EFBFBD><EFBFBD> |
2026-05-19 13:56:59 +00:00
|------|------|------|
| **SCIM <20><> ZITADEL <20><> Gateway** | <20><> HR / Entra ID / Okta | <20><><EFBFBD>~<7E><><EFBFBD>e<EFBFBD>ϥΪ<CFA5> |
| **ZITADEL LDAP IdP** | <20>Τ<EFBFBD><CEA4>n<EFBFBD>J<EFBFBD><4A> JIT | <20><><EFBFBD><EFBFBD><EFBFBD>n<EFBFBD>J<EFBFBD>إ<EFBFBD> member |
| **Directory Sync Worker** | <20>L SCIM <20><> AD / OpenLDAP | <20>w<EFBFBD>ɦP<C9A6>B + <20><>¾<EFBFBD><C2BE><EFBFBD><EFBFBD> |
2026-05-19 13:56:59 +00:00
### 10.2 LDAP <20>]<5D>w<EFBFBD>]AD + OpenLDAP<41>^
2026-05-19 13:56:59 +00:00
```go
type TenantLDAPConfig struct {
TenantID string
Type string // "ad" | "openldap"
Host string
Port int
UseTLS bool
BaseDN string
BindDN string // encrypted
BindPassword string // encrypted
UserFilter string
GroupFilter string
AttrMap LDAPAttrMap
}
type LDAPAttrMap struct {
Username string // AD: sAMAccountName / LDAP: uid
Email string // mail
DisplayName string // displayName / cn
Phone string // telephoneNumber
ExternalID string // objectGUID / entryUUID
Groups string // memberOf
}
```
### 10.3 SCIM
- **SCIM `id` = Gateway Member UID**<2A>]`AMEX-10000000`<60>^<5E>X <20>w<EFBFBD>M<EFBFBD><4D><EFBFBD>F<EFBFBD><48>B<EFBFBD><42><EFBFBD>t<EFBFBD>Τ@<40>P<EFBFBD>A<EFBFBD>K<EFBFBD><4B> audit/<2F><EFBFBD><E4B4A9><EFBFBD>e<EFBFBD>d<EFBFBD><64>
- SCIM `externalId` = <20>Ȥ<EFBFBD><C8A4><EFBFBD> IdP / HR <20>t<EFBFBD>δ<EFBFBD><CEB4>Ѫ<EFBFBD><D1AA>~<7E><><EFBFBD>ѧO<D1A7>]<5D>p Okta user id<69>BEntra object id<69>Bemployee id<69>^
- `externalId` <20>H `{tenant_id, external_id}` <20><> idempotent upsert key<65>F<EFBFBD><46><EFBFBD>i<EFBFBD><69><EFBFBD>]<5D>Ȥ<EFBFBD><C8A4>ݪ<EFBFBD><DDAA>D Gateway UID
- ZITADEL `sub`<EFBFBD>BMongo `_id` <20><><EFBFBD><EFBFBD><EFBFBD>~<7E>n<EFBFBD>S<EFBFBD>FZITADEL `sub` <20>z<EFBFBD>L SCIM Extension Schema `urn:cloudep:scim:2.0:User:zitadelSub` <20><><EFBFBD>Ѭd<D1AC>ߡA<DFA1>K<EFBFBD><4B><EFBFBD><EFBFBD><EFBFBD>~<7E><> troubleshoot
- SCIM Groups PATCH <20><> `permission.SyncFromScimGroup`
- SCIM deactivate <20><> `member.suspended` + `auth.RevokeAllForUser`
2026-05-19 13:56:59 +00:00
### 10.4 Directory Sync <20>~<7E>P<EFBFBD>O<EFBFBD>@<40>]<5D>w<EFBFBD>M<EFBFBD><4D><EFBFBD>^
2026-05-19 17:04:26 +00:00
| <20><><EFBFBD><EFBFBD> | <20>]<5D>w | <20>欰 |
2026-05-19 17:04:26 +00:00
|------|------|------|
| <20>s<EFBFBD><73><EFBFBD><EFBFBD><E4A4A3><EFBFBD>~<7E><><EFBFBD>v | `MissingThreshold: 3`<EFBFBD>]<5D>s<EFBFBD><73> 3 <20><> cron<6F>^ | <20>p<EFBFBD>Ʃ<EFBFBD> `members.directory_missing_count`<EFBFBD>F<EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Y<EFBFBD>k<EFBFBD>s |
| <20><EFBFBD><E6A6B8><EFBFBD>ʤW<CAA4><57> | `MaxChangeRatio: 0.20` | <20>榸 sync <20><><EFBFBD>ʶW<CAB6>L<EFBFBD>ӯ<EFBFBD><D3AF><EFBFBD> active members 20% <20><> **<EFBFBD>j<EFBFBD><EFBFBD><EFBFBD><EFBFBD> dry-run** + <20><69>A<EFBFBD>ݤH<DDA4>u<EFBFBD>T<EFBFBD>{ |
| <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>p | `DryRunOnFirstSync: true` | <20><><EFBFBD><EFBFBD><EFBFBD>P<EFBFBD>B<EFBFBD>u<EFBFBD>O diff log<6F>A**<2A><><EFBFBD>g DB** |
| Dry-run <20>Ҧ<EFBFBD> | `DryRun: true / false` | <20><><EFBFBD>{<7B><><EFBFBD>v<EFBFBD>T DB<44>A<EFBFBD>u<EFBFBD><75><EFBFBD>X diff <20><><EFBFBD><EFBFBD><EFBFBD>]admin API <20>i<EFBFBD>U<EFBFBD><55><EFBFBD>^ |
| <20>n<EFBFBD>R<EFBFBD>]<5D><>¾<EFBFBD>^ | guardrail <20><><EFBFBD>q<EFBFBD>L<EFBFBD>~ `status=suspended`<EFBFBD>]**<2A><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> deleted**<2A>^ | `deleted` <20>ݤH<DDA4>u<EFBFBD>αM<CEB1><4D> workflow |
| Sync window | `Window: 24h` | <20>w<EFBFBD>]<5D>C 24h<34>F<EFBFBD>i tenant override |
| <20><69>q<EFBFBD>D | `AlertSink: ops_webhook / mail` | IJ<>o dry-run / <20><><EFBFBD><EFBFBD><EFBFBD>ʲv / <20>s<EFBFBD>򥢱Ѯɳq<C9B3><71> |
2026-05-19 17:04:26 +00:00
> Worker <20>Ұʶ<D2B0><CAB6>ǡG<C7A1><47> LDAP snapshot <20><> <20>p<EFBFBD><70> diff <20><> <20>] guardrail <20>ˬd<CBAC>]threshold + ratio<69>^<5E><> commit <20><><EFBFBD><EFBFBD> dry-run <20><> <20>g audit log<6F>C
2026-05-19 17:04:26 +00:00
2026-05-19 13:56:59 +00:00
---
2026-05-19 17:04:26 +00:00
## 11. Notification Module
<EFBFBD><EFBFBD><EFBFBD>|<7C>G`internal/model/notification/`
2026-05-19 17:04:26 +00:00
<EFBFBD>W<EFBFBD><EFBFBD> model <20>ҲաA<D5A1><41><EFBFBD><EFBFBD><EFBFBD>B<EFBFBD>z<EFBFBD>Ҧ<EFBFBD> **outbound <20>q<EFBFBD>T**<EFBFBD>GEmail<EFBFBD>BSMS<EFBFBD>B<EFBFBD>]<5D>w<EFBFBD>d<EFBFBD>^Push<73>BWebhook<6F>C<EFBFBD>Ҧ<EFBFBD><D2A6>~<7E>ȼҲա]member <20>~<7E><><EFBFBD><EFBFBD><EFBFBD>ҡBauth step-up<75>Btenant <20>t<EFBFBD>γq<CEB3><71><EFBFBD>Badmin ĵ<>ܵ<EFBFBD><DCB5>^**<2A>Τ@**<2A>z<EFBFBD>L `NotifierUseCase` <20>o<EFBFBD>e<EFBFBD>A**<2A><>**<2A><><EFBFBD><EFBFBD> import provider SDK<44>C
2026-05-19 13:56:59 +00:00
### 11.1 ¾<>d
2026-05-19 13:56:59 +00:00
- Provider <20><><EFBFBD>H<EFBFBD>GEmail / SMS / Push / Webhook <20>i<EFBFBD>W<EFBFBD>ߴ<EFBFBD><DFB4><EFBFBD>
- Template <20><><EFBFBD>V<EFBFBD>G<EFBFBD>t<EFBFBD>h<EFBFBD>y<EFBFBD>t<EFBFBD>]i18n<38>^+ <20>ܼƪ`<60>J
- <20>P<EFBFBD>B<EFBFBD>o<EFBFBD>e<EFBFBD>P<EFBFBD><50><EFBFBD>B<EFBFBD>Ƶ{<7B>]idempotency + <20><><EFBFBD><EFBFBD> + DLQ<4C>^
- <20>q<EFBFBD><71><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Gpersist <20><> Mongo<67>]<5D>e<EFBFBD>F<EFBFBD><46><EFBFBD>A<EFBFBD>Bprovider message id<69>Bretry <20>y<EFBFBD><79><EFBFBD>^
- Rate limit / <20>t<EFBFBD>B<EFBFBD>]<5D><><EFBFBD>z<EFBFBD>o<EFBFBD>B<EFBFBD><42><EFBFBD>ݥΡ^
- Hook<6F>G<EFBFBD><47> audit log <20>P metrics <20>d<EFBFBD>I
2026-05-19 17:04:26 +00:00
### 11.2 <20>Ҳ<EFBFBD><D2B2><EFBFBD><EFBFBD><EFBFBD>
2026-05-19 17:04:26 +00:00
```
member / auth / tenant / admin
<20>x
<20><> (NotifierUseCase.Send / Enqueue)
notification <20>w<EFBFBD>w repository (audit + outbox)
<20>x
<20><> (interface)
2026-05-19 17:04:26 +00:00
internal/library/notification/
<20>u<EFBFBD>w<EFBFBD>w email/ (sendgrid | ses | smtp <20><><EFBFBD>@)
<20>u<EFBFBD>w<EFBFBD>w sms/ (twilio | sns | smsapi <20><><EFBFBD>@)
<20>|<7C>w<EFBFBD>w push/ (<28>w<EFBFBD>d)
2026-05-19 13:56:59 +00:00
```
**library <20>h**<2A>G<EFBFBD><47> IO<49>A<EFBFBD>ʸ˦U<CBA6>a SDK<44>F**model <20>h**<2A>G<EFBFBD>y<EFBFBD>{<7B>B<EFBFBD>ҪO<D2AA>Bretry<72>Baudit<69>Bidempotency<63>C
2026-05-19 17:04:26 +00:00
### 11.3 <20><><EFBFBD><EFBFBD>
2026-05-19 17:04:26 +00:00
```go
type NotifierUseCase interface {
// <20>P<EFBFBD>B<EFBFBD>o<EFBFBD>e<EFBFBD>G<EFBFBD><47><EFBFBD>o<EFBFBD><6F><EFBFBD>G<EFBFBD>P provider id<69>F<EFBFBD><46><EFBFBD>Ѧ^ error
2026-05-19 17:04:26 +00:00
Send(ctx context.Context, req *SendRequest) (*NotificationDTO, error)
// <20><><EFBFBD>B<EFBFBD>ƶ<EFBFBD><C6B6>G<EFBFBD>g Mongo outbox + <20>J channel<65>Aworker <20>Ԩ<EFBFBD><D4A8><EFBFBD><EFBFBD>աF<D5A1><46><EFBFBD>]<5D>R<EFBFBD><52>
2026-05-19 17:04:26 +00:00
Enqueue(ctx context.Context, req *SendRequest) (*NotificationDTO, error)
// <20>d<EFBFBD>߳浧<DFB3><E6B5A7><EFBFBD>A
2026-05-19 17:04:26 +00:00
Get(ctx context.Context, tenantID, notificationID string) (*NotificationDTO, error)
}
type SendRequest struct {
TenantID string
UID string // <20>i<EFBFBD><69><EFBFBD>š]<5D>t<EFBFBD>γq<CEB3><71><EFBFBD>^
2026-05-19 17:04:26 +00:00
Channel enum.Channel // email | sms | push | webhook
Kind enum.NotifyKind // verify_email | verify_phone | step_up | system_alert | ...
Target string // <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>}<7D>]email / phone / device_token / url<72>^
Locale string // zh-tw | en-us<75>A<EFBFBD><41><EFBFBD><EFBFBD><EFBFBD>w<EFBFBD><77> tenant.default_locale
Data map[string]any // <20>ҪO<D2AA>ܼ<EFBFBD>
2026-05-19 17:04:26 +00:00
Severity enum.Severity // info | warn | critical
IdempotencyKey string // <20>~<7E><> key<65>F<EFBFBD>P key <20><><EFBFBD><EFBFBD><EFBFBD>o
DoNotPersistBody bool // OTP <20><><EFBFBD>ӷP<D3B7><50><EFBFBD>e<EFBFBD><65><EFBFBD>J<EFBFBD>w<EFBFBD>A<EFBFBD>u<EFBFBD>O metadata
2026-05-19 17:04:26 +00:00
}
2026-05-19 13:56:59 +00:00
```
> **OTP <20><><EFBFBD>ӷP<D3B7><50><EFBFBD>e**<2A>G`DoNotPersistBody=true` <20><> notification.body <20>d<EFBFBD>šA<C5A1>u<EFBFBD>O channel/kind/target hash/provider_message_id/status<75>A<EFBFBD>קK audit DB <20>X<EFBFBD>{<7B><><EFBFBD>X OTP<54>C
2026-05-19 13:56:59 +00:00
### 11.4 Entity <20>P Collection
2026-05-19 13:56:59 +00:00
2026-05-19 17:04:26 +00:00
```go
// notification collection
type Notification struct {
ID primitive.ObjectID
TenantID string
UID string
Channel enum.Channel
Kind enum.NotifyKind
TargetHash string // sha256(target)<29>A<EFBFBD>קK<D7A7><4B><EFBFBD>X PII
TemplateKey string // <20><><EFBFBD><EFBFBD> TemplateRegistry
2026-05-19 17:04:26 +00:00
Locale string
Provider string // "sendgrid" | "twilio" | ...
ProviderMessageID string
Status enum.NotifyStatus // pending | sent | failed | retrying | dropped
Attempts int
LastError string
IdempotencyKey string // <20>ߤ@<40><><EFBFBD><EFBFBD> {tenant_id, kind, idempotency_key}
2026-05-19 17:04:26 +00:00
Severity enum.Severity
OccurredAt int64
DeliveredAt int64
}
2026-05-19 13:56:59 +00:00
```
2026-05-19 17:04:26 +00:00
**Template** <20><> **in-code registry**<EFBFBD>]<5D><><EFBFBD>O<EFBFBD>w<EFBFBD><77><EFBFBD>^+ provider <20>ݼҪO ID<49>]<5D>p SendGrid Dynamic Template<74>^<5E>G
2026-05-19 17:04:26 +00:00
```go
var TemplateRegistry = map[enum.NotifyKind]TemplateSpec{
enum.NotifyVerifyEmail: {
EmailProviderTemplateID: "d-xxxxxxxxxxxxx", // SendGrid
SMSText: "",
RequiredVars: []string{"code", "expires_in"},
},
enum.NotifyVerifyPhone: {
SMSText: "Your verification code is {code} (valid {expires_in}s)",
RequiredVars: []string{"code", "expires_in"},
},
enum.NotifyStepUpEmail: {...},
enum.NotifyStepUpPhone: {...},
enum.NotifySystemAlert: {...},
}
2026-05-19 13:56:59 +00:00
```
### 11.5 Idempotency <20>P<EFBFBD><50><EFBFBD><EFBFBD>
2026-05-19 17:04:26 +00:00
- `IdempotencyKey` <20>ߤ@<40><><EFBFBD>ޡG`{TenantID, Kind, IdempotencyKey}`
- <20><><EFBFBD><EFBFBD> Send <20>P key <20><> <20><><EFBFBD><EFBFBD><EFBFBD>^<5E>W<EFBFBD><57><EFBFBD><EFBFBD><EFBFBD>G<EFBFBD>]<5D><><EFBFBD><EFBFBD><EFBFBD>o<EFBFBD><6F> provider<65>^
- <20><><EFBFBD>B worker <20><><EFBFBD>ѵ<EFBFBD><D1B5><EFBFBD><EFBFBD>G<EFBFBD><47><EFBFBD>ưh<C6B0><68> 1s / 5s / 30s / 5min / 30min<69>A<EFBFBD>̦h 5 <20><><EFBFBD>F<EFBFBD>W<EFBFBD>L <20><> `status=dropped` + audit
- DLQ<4C>G<EFBFBD><47><EFBFBD><EFBFBD> 5 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>O<EFBFBD>d<EFBFBD>b `notification_dlq` collection<6F>Aadmin API <20>i<EFBFBD><69><EFBFBD><EFBFBD> retry
2026-05-19 17:04:26 +00:00
### 11.6 <20>P<EFBFBD>~<7E>ȼҲժ<D2B2><D5AA>I<EFBFBD>s<EFBFBD><73><EFBFBD>Y
2026-05-19 17:04:26 +00:00
| <20>I<EFBFBD>s<EFBFBD><73> | Kind | Channel | <20>Ҧ<EFBFBD> |
2026-05-19 17:04:26 +00:00
|--------|------|---------|------|
| `member.VerificationUseCase` | `verify_email` / `verify_phone` | email / sms | **<EFBFBD>P<EFBFBD>B**<2A>]<5D>n<EFBFBD>ߧY<DFA7><59><EFBFBD>D<EFBFBD>e<EFBFBD>F / <20><><EFBFBD>ѡ^ |
| `member.StepUpUseCase` | `step_up_email` / `step_up_phone` | email / sms | **<EFBFBD>P<EFBFBD>B** |
| `member.AdminUseCase`<EFBFBD>]<5D><><EFBFBD>v<EFBFBD>i<EFBFBD><69><EFBFBD>^ | `account_suspended` | email | **<EFBFBD><EFBFBD><EFBFBD>B** |
| `tenant.UseCase`<EFBFBD>]<5D><><EFBFBD><EFBFBD><EFBFBD>إߧ<D8A5><DFA7><EFBFBD><EFBFBD>^ | `tenant_welcome` | email | **<EFBFBD><EFBFBD><EFBFBD>B** |
| ops alert<72>]<5D><><EFBFBD><EFBFBD><EFBFBD>ʲv / DLQ <20><><EFBFBD>^ | `ops_alert` | email / webhook | **<EFBFBD>P<EFBFBD>B**<2A>]critical<61>^ |
2026-05-19 17:04:26 +00:00
> **OTP <20><><EFBFBD><EFBFBD><EFBFBD>P<EFBFBD>B**<2A>A<EFBFBD>_<EFBFBD>h client <20>L<EFBFBD>k<EFBFBD>^<5E><><EFBFBD>uOTP <20>w<EFBFBD>H<EFBFBD>X<EFBFBD>v<EFBFBD><76><EFBFBD><EFBFBD><EFBFBD>T<EFBFBD><54><EFBFBD>~<7E>F<EFBFBD><46><EFBFBD>L<EFBFBD>q<EFBFBD><71><EFBFBD>u<EFBFBD><75><EFBFBD><EFBFBD><EFBFBD>B<EFBFBD>קK<D7A7><4B><EFBFBD>C<EFBFBD>~<7E><> API<50>C
2026-05-19 17:04:26 +00:00
### 11.7 <20>P Audit Log <20><><EFBFBD><EFBFBD><EFBFBD>Y
2026-05-19 17:04:26 +00:00
<EFBFBD>C<EFBFBD><EFBFBD> Notification <20>g<EFBFBD>J<EFBFBD>ɦP<C9A6>B<EFBFBD>g audit log<6F>G
2026-05-19 17:04:26 +00:00
```
action = notification.sent | notification.failed | notification.dropped
actor = system <20><> caller uid
2026-05-19 17:04:26 +00:00
target = { kind: notification, id: notification_id, channel, kind }
metadata = { provider, provider_message_id, target_hash }
```
audit log <20><><EFBFBD><EFBFBD><EFBFBD>Ʀs body<64>]<5D>w<EFBFBD>M<EFBFBD><4D> <20><>20.1 critical <20>P<EFBFBD>B<EFBFBD>g<EFBFBD><67><EFBFBD>d<EFBFBD><64>**<2A><><EFBFBD>t**<2A>q<EFBFBD><71><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>A<EFBFBD>Ȥ<EFBFBD><C8A4>ƾڡ^<5E>C
2026-05-19 17:04:26 +00:00
### 11.8 <20>w<EFBFBD><77><EFBFBD>P PII
2026-05-19 17:04:26 +00:00
- `Target` <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> persist<73>F<EFBFBD>s `TargetHash`<EFBFBD>]sha256<35>^<5E>A<EFBFBD>K<EFBFBD><4B><EFBFBD>h<EFBFBD><68><EFBFBD>Bidempotency<63>F<EFBFBD><46><EFBFBD>X<EFBFBD>Ȧb send <20><><EFBFBD>U<EFBFBD>ǵ<EFBFBD> provider
- Email/SMS provider API key<65>BTwilio token <20><> <20><> `etc/gateway.yaml` <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ܼ<EFBFBD> + secret manager
- Webhook <20>q<EFBFBD>D<EFBFBD>j<EFBFBD><6A> HTTPS + HMAC ñ<><C3B1><EFBFBD>]`X-CloudEP-Signature`<60>^
2026-05-19 17:04:26 +00:00
---
## 12. <20>iŪ UID <20>]<5D>p<EFBFBD>]<5D>w<EFBFBD>M<EFBFBD><4D><EFBFBD>^
2026-05-19 17:04:26 +00:00
### 12.1 <20>
2026-05-19 17:04:26 +00:00
```
{UIDPrefix}-{Sequence}
<EFBFBD>d<EFBFBD>ҡGAMEX-10000000<30>BACME-10000001<30>BACME-10000002
2026-05-19 17:04:26 +00:00
```
**<2A>w<EFBFBD>M<EFBFBD><4D><EFBFBD>G<EFBFBD>a<EFBFBD><61><EFBFBD><EFBFBD><EFBFBD>e<EFBFBD><65>**<2A>]<5D><><EFBFBD>ί<EFBFBD> Body<64>B<EFBFBD><42><EFBFBD><EFBFBD> UUID<49>^<5E>C
2026-05-19 17:04:26 +00:00
| <20><><EFBFBD><EFBFBD> | <20>W<EFBFBD>h | <20>d<EFBFBD><64> |
2026-05-19 17:04:26 +00:00
|------|------|------|
| `UIDPrefix` | 2~4 <20><><EFBFBD>j<EFBFBD>g<EFBFBD>A<EFBFBD>Ӧ<EFBFBD> `tenant.UIDPrefix` <20><> slug <20>Y<EFBFBD>g | `AMEX`<EFBFBD>B`ACME` |
| `Sequence` | <20>Q<EFBFBD>i<EFBFBD><EFBFBD>W<EFBFBD><57><EFBFBD>ơA**<2A>_<EFBFBD>l `10000000`**<2A>]<5D>u<EFBFBD><75> `InitAutoID` <20>y<EFBFBD>N<EFBFBD>^ | `10000000` |
| <20><><EFBFBD>j<EFBFBD><6A> | <20>T<EFBFBD>w `-` | `AMEX-10000000` |
2026-05-19 17:04:26 +00:00
- <20>H<EFBFBD><48><EFBFBD><69>B<EFBFBD>ȪA<C8AA>i<EFBFBD>v<EFBFBD>r<EFBFBD>f<EFBFBD>z
- <20><><EFBFBD>t UUID / base64 <20>ýX
- **`UIDPrefix` <20><><EFBFBD><EFBFBD><EFBFBD>x<EFBFBD>ߤ@**<2A>]<5D>w<EFBFBD>M<EFBFBD><4D><EFBFBD>^<5E>F<EFBFBD>ȪA<C8AA><41><EFBFBD>J UID <20>Y<EFBFBD>i<EFBFBD>w<EFBFBD><77> tenant + member
- <20><><EFBFBD>P<EFBFBD><50><EFBFBD><EFBFBD>i<EFBFBD>ۦP `UIDPrefix`<EFBFBD>F<EFBFBD>P prefix <20><> Sequence <20>q `10000000` <20>_<EFBFBD><5F>
2026-05-19 17:04:26 +00:00
### 12.2 <20><><EFBFBD>͡]Bucket <20><><EFBFBD><EFBFBD><EFBFBD>A<EFBFBD><EFBFBD><EFBFBD><E6AFB2> 50 <20>U<EFBFBD>^
2026-05-19 13:56:59 +00:00
```
Redis: member:seq:{tenant_id} counter<65>A<EFBFBD><41><EFBFBD>l 10000000
<EFBFBD>C<EFBFBD><EFBFBD> pod <20>ҰʩίӺɮ<D3BA> INCRBY 500 <20><><EFBFBD>@<40><> bucket<65>A<EFBFBD>b<EFBFBD>O<EFBFBD><4F><EFBFBD><EFBFBD><E9A4BA><EFBFBD><EFBFBD>
2026-05-19 17:04:26 +00:00
UID = tenant.UIDPrefix + "-" + strconv.FormatInt(sequence, 10)
2026-05-19 13:56:59 +00:00
```
- **<2A>õo<C3B5>O<EFBFBD>@**<2A>G`{ tenant_id, uid }` unique index<65>C`EnsureFromOIDC` / `EnsureFromLDAP` / `EnsureFromSCIM` / `CreateUnverified` <20>R<EFBFBD><52> dup key<65>]E11000<30>^<5E><> fallback `GetByZitadelUserID` <20><> `GetByEmail` <20><><EFBFBD>J<EFBFBD><4A> member<65>C
- **Pod crash <20>e<EFBFBD><65>**<2A>Gbucket <20><><EFBFBD><EFBFBD><EFBFBD>Χ<EFBFBD><CEA7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>i<EFBFBD><69><EFBFBD><EFBFBD><EFBFBD>]UID <20><><EFBFBD>n<EFBFBD>D<EFBFBD>Y<EFBFBD><59><EFBFBD>s<EFBFBD><73><EFBFBD>B<EFBFBD><42><EFBFBD>n<EFBFBD>D<EFBFBD>Y<EFBFBD><EFBFBD>W<EFBFBD>F<EFBFBD>u<EFBFBD>n<EFBFBD>D<EFBFBD><44><EFBFBD><EFBFBD>ߤ@<40>^<5E>C
- **UIDPrefix unique index**<2A>G`tenants.{ uid_prefix: 1 } unique`<60>F<EFBFBD>د<EFBFBD><D8AF><EFBFBD><EFBFBD>ɭY prefix <20>w<EFBFBD>s<EFBFBD>b <20><> 409<30>C
2026-05-19 13:56:59 +00:00
---
## 13. <20><><EFBFBD>Ƽҫ<C6BC><D2AB>P<EFBFBD><50><EFBFBD><EFBFBD>
2026-05-19 13:56:59 +00:00
2026-05-19 17:04:26 +00:00
### 13.1 Collections
2026-05-19 13:56:59 +00:00
| Collection | <20>Ҳ<EFBFBD> | <20><><EFBFBD><EFBFBD> |
2026-05-19 13:56:59 +00:00
|------------|------|------|
| `members` | member | Profile<6C>]<5D>t<EFBFBD>~<7E><><EFBFBD><EFBFBD><EFBFBD>ҺX<D2BA>СBTOTP cipher<65>BOrigin<69>^ |
| `identities` | member | zitadel_sub ? uid |
| `tenants` | member | <20><><EFBFBD><EFBFBD> metadata |
| `tenant_ldap_configs` | member | LDAP <20>P<EFBFBD>B<EFBFBD>]<5D>w<EFBFBD>]<5D>[<5B>K<EFBFBD>^ |
| `permissions` | permission | <20><><EFBFBD><EFBFBD> Permission Tree<65>]<5D><><EFBFBD>x seed<65>^ |
| `roles` | permission | <20><><EFBFBD><EFBFBD> Role<6C>]`tenant_id` + immutable `key`<EFBFBD>^ |
| `role_permissions` | permission | Role ? Permission ID |
| `user_roles` | permission | uid ? Role<6C>]<5D><EFBFBD>h<EFBFBD><68><EFBFBD><EFBFBD><EFBFBD>^ |
| `role_mappings` | permission | <20>~<7E><> Group ? RoleID / Role.Key |
| `notifications` | notification | <20>q<EFBFBD><71><EFBFBD>o<EFBFBD>e<EFBFBD><65><EFBFBD><EFBFBD><EFBFBD>]idempotency / <20><><EFBFBD><EFBFBD> / audit<69>^ |
| `notification_dlq` | notification | <20><><EFBFBD><EFBFBD> 5 <20><><EFBFBD><EFBFBD><EFBFBD>Ѫ<EFBFBD> dead letter queue |
| `audit_logs` | <20>]<5D>W<EFBFBD><57> DB<44>^| <20><><EFBFBD>Ҳռf<D5BC>p<EFBFBD><70><EFBFBD>x<EFBFBD>]TTL 90d<30>A<EFBFBD><41>20.1<EFBFBD>^ |
### 13.2 <20>D<EFBFBD>n<EFBFBD><6E><EFBFBD><EFBFBD>
2026-05-19 13:56:59 +00:00
```javascript
// members
{ tenant_id: 1, uid: 1 } // unique
{ tenant_id: 1, zitadel_user_id: 1 } // unique
{ tenant_id: 1, member_status: 1, create_at: -1 }
// identities
{ tenant_id: 1, zitadel_user_id: 1 } // unique
{ tenant_id: 1, uid: 1 }
{ tenant_id: 1, external_id: 1 }
// permissions<6E>]<5D><><EFBFBD><EFBFBD><EFBFBD>^
2026-05-19 13:56:59 +00:00
{ name: 1 } // unique
{ parent: 1, status: 1 }
{ http_path: 1, http_method: 1 } // sparse
// roles
2026-05-19 17:04:26 +00:00
{ tenant_id: 1, key: 1 } // unique
2026-05-19 13:56:59 +00:00
{ tenant_id: 1, status: 1 }
// role_permissions
2026-05-19 17:04:26 +00:00
{ tenant_id: 1, role_id: 1, permission_id: 1 } // unique
2026-05-19 13:56:59 +00:00
// user_roles
{ tenant_id: 1, uid: 1, role_id: 1 } // unique
{ tenant_id: 1, uid: 1 }
// role_mappings
{ tenant_id: 1, external_source: 1, external_key: 1 } // unique
2026-05-19 17:04:26 +00:00
{ tenant_id: 1, internal_role_id: 1 }
// notifications
{ tenant_id: 1, kind: 1, idempotency_key: 1 } // unique<75>]<5D>P key <20><><EFBFBD><EFBFBD><EFBFBD>o<EFBFBD>^
2026-05-19 17:04:26 +00:00
{ tenant_id: 1, uid: 1, occurred_at: -1 }
{ status: 1, attempts: 1, occurred_at: 1 } // worker <20><><EFBFBD>ݭ<EFBFBD><DDAD><EFBFBD>
2026-05-19 17:04:26 +00:00
// notification_dlq
{ tenant_id: 1, occurred_at: -1 }
// audit_logs<67>]<5D>W<EFBFBD><57> DB / replica set<65>^
2026-05-19 17:04:26 +00:00
{ tenant_id: 1, occurred_at: -1 }
{ tenant_id: 1, "actor.uid": 1, occurred_at: -1 }
{ tenant_id: 1, action: 1, occurred_at: -1 }
{ occurred_at: 1 } // TTL 90d
2026-05-19 13:56:59 +00:00
```
> Identity <20>M<EFBFBD>g<EFBFBD>H `identities` collection <20><> source of truth<74>F`members.zitadel_user_id` <20>Y<EFBFBD>O<EFBFBD>d<EFBFBD>A<EFBFBD>u<EFBFBD>@<40>Ϭd<CFAC>֨<EFBFBD>/denormalized <20><><EFBFBD><EFBFBD><EFBFBD>A<EFBFBD><41><EFBFBD>s<EFBFBD>ݥѦP<D1A6>@ transaction <20>θ<EFBFBD><CEB8>v<EFBFBD>y<EFBFBD>{<7B><><EFBFBD><EFBFBD><EFBFBD>@<40>P<EFBFBD>C
2026-05-19 17:04:26 +00:00
> **<2A>ɶ<EFBFBD><C9B6><EFBFBD><EFBFBD><EFBFBD>**<2A>G`CreateAt` / `UpdateAt` <20>Τ@<40><> **epoch milliseconds<64>]UTC<54>^**<2A>C<EFBFBD><43><EFBFBD>~ SCIM `meta.created` / `meta.lastModified` <20><> SCIM mapper <20>b<EFBFBD>ǦC<C7A6>Ʈ<EFBFBD><C6AE><EFBFBD> RFC3339Nano<6E>F<EFBFBD>e<EFBFBD>ݮi<DDAE>ܥ<EFBFBD> client <20>t<EFBFBD>d timezone<6E>C
2026-05-19 17:04:26 +00:00
### 13.3 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]100 <20>U+<2B>^
2026-05-19 13:56:59 +00:00
```
Shard Key: { tenant_id: 1, uid: 1 }
```
<EFBFBD><EFBFBD><EFBFBD> 50 <20>U<EFBFBD>|<7C><><EFBFBD><EFBFBD><EFBFBD>b<EFBFBD>P<EFBFBD>@ chunk<6E>AMongoDB <20><><EFBFBD>i<EFBFBD>Ө<EFBFBD><D3A8>F<EFBFBD>Y<EFBFBD>w<EFBFBD><77><EFBFBD><EFBFBD><E6AFB2><EFBFBD>d<EFBFBD>U<EFBFBD>ŦA<C5A6><41><EFBFBD><EFBFBD> hash <20>G<EFBFBD><47><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>C
2026-05-19 13:56:59 +00:00
---
## 14. Redis Key <20>R<EFBFBD>W
2026-05-19 13:56:59 +00:00
### auth<74>]`internal/model/auth/redis.go`<60>^
2026-05-19 13:56:59 +00:00
```
auth:jwt:bl:{jti} # <20><> token <20>¦W<C2A6><57><EFBFBD>ATTL = <20>Ѿl<D1BE>ةR
auth:jwt:pair:{access_jti} # access_jti <20><> refresh_jti<74>]<5D>n<EFBFBD>X<EFBFBD>ɳs refresh <20>@<40>_<EFBFBD>Զ¡^
auth:gen:{tenant_id}:{uid} # <20><><EFBFBD>q<EFBFBD><71><EFBFBD>ĥN<C4A5><4E>
auth:exchange:nonce:{id_token_jti} # Token Exchange <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ATTL 10min
auth:stepup:used:{jti} # Step-up token <20><EFBFBD>ʡATTL = step_up_token TTL
2026-05-19 13:56:59 +00:00
```
### member<65>]`internal/model/member/redis.go`<60>^
2026-05-19 13:56:59 +00:00
```
member:profile:{tenant_id}:{uid} # profile cache<68>ATTL 5~15min
member:sub:{tenant_id}:{sub} # zitadel_sub <20><> uid<69>ATTL 1h
2026-05-19 13:56:59 +00:00
member:seq:{tenant_id} # UID bucket counter
2026-05-19 17:04:26 +00:00
otp:challenge:{tenant_id}:{challenge_id} # {purpose, identifier, code_hash, attempts, expire_at}<7D>ATTL 5min
otp:rate:{tenant_id}:{purpose}:{identifier} # <20><><EFBFBD>o<EFBFBD>N<EFBFBD>o 60s
otp:daily:{tenant_id}:{purpose}:{identifier} # <20><><EFBFBD><EFBFBD><EFBFBD>W<EFBFBD><57> INCR<43>ATTL 24h
2026-05-19 17:04:26 +00:00
totp:enroll:{tenant_id}:{uid} # enroll <20>Ȧs secret_cipher<65>ATTL 10min
totp:used:{tenant_id}:{uid}:{timestep} # TOTP code <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ATTL 90s
2026-05-19 17:04:26 +00:00
```
### notification<6F>]`internal/model/notification/redis.go`<60>^
2026-05-19 17:04:26 +00:00
```
notif:idem:{tenant_id}:{kind}:{idempotency_key} # idempotency <20><><EFBFBD>G<EFBFBD>֨<EFBFBD><D6A8>ATTL 24h
notif:quota:{tenant_id}:{channel} # <20>C<EFBFBD><43><EFBFBD><EFBFBD><EFBFBD>C<EFBFBD>q<EFBFBD>D quota<74>AINCR + TTL
notif:retry:zset # <20><><EFBFBD>B<EFBFBD><42><EFBFBD>ձƵ{<7B>]score = next_retry_at_ms<6D>^
2026-05-19 13:56:59 +00:00
```
### permission<6F>]`internal/model/permission/redis.go`<60>^
2026-05-19 13:56:59 +00:00
```
permission:casbin:rules:{tenant_id} # Casbin policy rules<65>]List of JSON<4F>^
permission:tree:open # <20>i<EFBFBD><69><EFBFBD>Gopen <20>`<60>I cache
perm:role_perms:{tenant_id}:{role_id} # role <20><> permission names<65>ATTL 30min
perm:user_roles:{tenant_id}:{uid} # uid <20><> role keys<79>ATTL 5min
2026-05-19 13:56:59 +00:00
```
---
## 15. <20>W<EFBFBD>һP<D2BB>ʯ<EFBFBD><CAAF>]100 <20>U+ / <20><EFBFBD><E6AFB2> 50 <20>U<EFBFBD>^
2026-05-19 13:56:59 +00:00
| <20><><EFBFBD><EFBFBD> | <20><><EFBFBD><EFBFBD> |
2026-05-19 13:56:59 +00:00
|------|------|
| Gateway | <20>L<EFBFBD><4C><EFBFBD>A<EFBFBD>A<EFBFBD><41><EFBFBD><EFBFBD><EFBFBD>X<EFBFBD>i |
| MongoDB | Sharding + Replica Set<65><41><C5AA> secondary |
| ListMembers | Cursor <20><><EFBFBD><EFBFBD><EFBFBD>A<EFBFBD>T<EFBFBD><54> deep offset |
| Authorize | Casbin EnforceEx<45>]?<3F>s + Redis policy<63>^ |
| LoadPolicy | ?<3F><>?<3F>W<EFBFBD>q<EFBFBD>Fcron 5min <20><><EFBFBD>q<EFBFBD>©<EFBFBD> |
| JWT <20><> UID | Redis cache 1h |
| Directory Sync | 500 users / batch<63>Arate limit ZITADEL API |
| Access Token TTL | 15min<69>]<5D><><EFBFBD>C<EFBFBD>M<EFBFBD>P<EFBFBD><50><EFBFBD>f<EFBFBD>^ |
2026-05-19 13:56:59 +00:00
### <20>e<EFBFBD>q<EFBFBD>ʦ<EFBFBD>
2026-05-19 13:56:59 +00:00
```
100 <20>U members <20><> ~2KB ? 2GB<47>]<5D><><EFBFBD>t index<65>^
indexes ? 1~2GB
<EFBFBD><EFBFBD> <20><EFBFBD>s<EFBFBD>i<EFBFBD>Ө<EFBFBD><D3A8>A<EFBFBD><41>ij 3 node replica set <20>_<EFBFBD><5F>
2026-05-19 13:56:59 +00:00
```
---
## 16. <20>ؿ<EFBFBD><D8BF><EFBFBD><EFBFBD>c
2026-05-19 13:56:59 +00:00
```
gateway/
<EFBFBD>u<EFBFBD>w<EFBFBD>w generate/api/
<EFBFBD>x <20>u<EFBFBD>w<EFBFBD>w auth.api
<EFBFBD>x <20>u<EFBFBD>w<EFBFBD>w member.api
<EFBFBD>x <20>u<EFBFBD>w<EFBFBD>w permission.api
<EFBFBD>x <20>u<EFBFBD>w<EFBFBD>w tenant.api
<EFBFBD>x <20>|<7C>w<EFBFBD>w scim.api
<EFBFBD>x
<EFBFBD>u<EFBFBD>w<EFBFBD>w internal/
<EFBFBD>x <20>u<EFBFBD>w<EFBFBD>w middleware/
<EFBFBD>x <20>x <20>u<EFBFBD>w<EFBFBD>w jwt_revoke.go
<EFBFBD>x <20>x <20>u<EFBFBD>w<EFBFBD>w tenant_context.go
<EFBFBD>x <20>x <20>u<EFBFBD>w<EFBFBD>w require_permission.go
<EFBFBD>x <20>x <20>|<7C>w<EFBFBD>w scim_auth.go
<EFBFBD>x <20>x
<EFBFBD>x <20>u<EFBFBD>w<EFBFBD>w library/
<EFBFBD>x <20>x <20>u<EFBFBD>w<EFBFBD>w zitadel/
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w oidc.go
<EFBFBD>x <20>x <20>x <20>|<7C>w<EFBFBD>w management.go
<EFBFBD>x <20>x <20>u<EFBFBD>w<EFBFBD>w ldap/
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w client.go
<EFBFBD>x <20>x <20>x <20>|<7C>w<EFBFBD>w attrmap.go
<EFBFBD>x <20>x <20>u<EFBFBD>w<EFBFBD>w casbin/ # Enforcer <20><><EFBFBD>l<EFBFBD><6C> helper
<EFBFBD>x <20>x <20>u<EFBFBD>w<EFBFBD>w uid/
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w encode.go
<EFBFBD>x <20>x <20>x <20>|<7C>w<EFBFBD>w generator.go
<EFBFBD>x <20>x <20>u<EFBFBD>w<EFBFBD>w totp/ # RFC 6238 <20>t<EFBFBD><74><EFBFBD>k<EFBFBD>BQR <20>ͦ<EFBFBD>
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w totp.go
<EFBFBD>x <20>x <20>x <20>|<7C>w<EFBFBD>w backup_code.go
<EFBFBD>x <20>x <20>u<EFBFBD>w<EFBFBD>w crypto/ # AES-GCM secret <20>[<5B>ѱK + KMS
<EFBFBD>x <20>x <20>x <20>|<7C>w<EFBFBD>w secret.go
<EFBFBD>x <20>x <20>|<7C>w<EFBFBD>w notification/ # Provider <20><><EFBFBD>@<40>]<5D><> IO <20>ʸˡ^
<EFBFBD>x <20>x <20>u<EFBFBD>w<EFBFBD>w email/
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w sendgrid.go
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w ses.go
<EFBFBD>x <20>x <20>x <20>|<7C>w<EFBFBD>w smtp.go
<EFBFBD>x <20>x <20>u<EFBFBD>w<EFBFBD>w sms/
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w twilio.go
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w sns.go
<EFBFBD>x <20>x <20>x <20>|<7C>w<EFBFBD>w smsapi.go
<EFBFBD>x <20>x <20>|<7C>w<EFBFBD>w push/ # <20>w<EFBFBD>d
<EFBFBD>x <20>x
<EFBFBD>x <20>u<EFBFBD>w<EFBFBD>w model/
<EFBFBD>x <20>x <20>u<EFBFBD>w<EFBFBD>w auth/
<EFBFBD>x <20>x <20>x <20>|<7C>w<EFBFBD>w ...
<EFBFBD>x <20>x <20>u<EFBFBD>w<EFBFBD>w member/ # <20>t verification / step_up / totp usecase
<EFBFBD>x <20>x <20>x <20>|<7C>w<EFBFBD>w ...
<EFBFBD>x <20>x <20>u<EFBFBD>w<EFBFBD>w notification/ # <20>Τ@<40>q<EFBFBD><71><EFBFBD>J<EFBFBD>f
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w entity/
<EFBFBD>x <20>x <20>x <20>x <20>|<7C>w<EFBFBD>w notification.go
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w enum/
<EFBFBD>x <20>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w channel.go
<EFBFBD>x <20>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w kind.go
<EFBFBD>x <20>x <20>x <20>x <20>|<7C>w<EFBFBD>w status.go
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w repository/
<EFBFBD>x <20>x <20>x <20>x <20>|<7C>w<EFBFBD>w notification.go
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w usecase/
<EFBFBD>x <20>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w notifier.go
<EFBFBD>x <20>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w template.go
<EFBFBD>x <20>x <20>x <20>x <20>|<7C>w<EFBFBD>w worker.go
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w config/
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w errors.go
<EFBFBD>x <20>x <20>x <20>|<7C>w<EFBFBD>w redis.go
<EFBFBD>x <20>x <20>|<7C>w<EFBFBD>w permission/
<EFBFBD>x <20>x <20>u<EFBFBD>w<EFBFBD>w entity/
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w permission.go
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w role.go
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w user_role.go
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w role_permission.go
<EFBFBD>x <20>x <20>x <20>|<7C>w<EFBFBD>w role_mapping.go
<EFBFBD>x <20>x <20>u<EFBFBD>w<EFBFBD>w enum/
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w status.go
<EFBFBD>x <20>x <20>x <20>|<7C>w<EFBFBD>w permission_type.go
<EFBFBD>x <20>x <20>u<EFBFBD>w<EFBFBD>w repository/
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w permission.go
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w role.go
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w user_role.go
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w role_permission.go
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w role_mapping.go
<EFBFBD>x <20>x <20>x <20>|<7C>w<EFBFBD>w casbin_redis_adapter.go # <20>u<EFBFBD><75> permission-server
<EFBFBD>x <20>x <20>u<EFBFBD>w<EFBFBD>w usecase/
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w permission_tree.go # <20>u<EFBFBD><75> permission-server
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w rbac.go # Casbin LoadPolicy / Check
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w permission.go
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w role.go
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w role_permission.go
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w user_role.go
<EFBFBD>x <20>x <20>x <20>u<EFBFBD>w<EFBFBD>w role_mapping.go
<EFBFBD>x <20>x <20>x <20>|<7C>w<EFBFBD>w authorization_query.go
<EFBFBD>x <20>x <20>u<EFBFBD>w<EFBFBD>w rbac/
<EFBFBD>x <20>x <20>x <20>|<7C>w<EFBFBD>w rule.go # Casbin Rule struct
<EFBFBD>x <20>x <20>u<EFBFBD>w<EFBFBD>w config/
<EFBFBD>x <20>x <20>u<EFBFBD>w<EFBFBD>w errors.go
<EFBFBD>x <20>x <20>u<EFBFBD>w<EFBFBD>w redis.go
<EFBFBD>x <20>x <20>|<7C>w<EFBFBD>w mock/
<EFBFBD>x <20>x
<EFBFBD>x <20>|<7C>w<EFBFBD>w worker/
<EFBFBD>x <20>u<EFBFBD>w<EFBFBD>w directory_sync/
<EFBFBD>x <20>u<EFBFBD>w<EFBFBD>w policy_sync/ # <20>i<EFBFBD><69><EFBFBD>G<EFBFBD>w<EFBFBD><77> LoadPolicy
<EFBFBD>x <20>u<EFBFBD>w<EFBFBD>w notification_retry/ # <20><><EFBFBD>B<EFBFBD><42><EFBFBD>աBDLQ <20><><EFBFBD><EFBFBD>
<EFBFBD>x <20>|<7C>w<EFBFBD>w member_anonymize/ # <20>n<EFBFBD>R 30 <20>ѫ<EFBFBD><D1AB>ΦW<CEA6>ơ]<5D><>5.7<EFBFBD>^
<EFBFBD>x
<EFBFBD>u<EFBFBD>w<EFBFBD>w etc/
<EFBFBD>x <20>u<EFBFBD>w<EFBFBD>w gateway.yaml
<EFBFBD>x <20>|<7C>w<EFBFBD>w rbac.conf # Casbin <20>ҫ<EFBFBD><D2AB>]<5D>u<EFBFBD><75> permission-server<65>^
<EFBFBD>x
<EFBFBD>|<7C>w<EFBFBD>w docs/
<20>u<EFBFBD>w<EFBFBD>w model.md
<20>|<7C>w<EFBFBD>w identity-member-design.md # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
2026-05-19 13:56:59 +00:00
```
---
## 17. <20>]<5D>w<EFBFBD><77>
2026-05-19 13:56:59 +00:00
`etc/gateway.yaml` <20>X<EFBFBD>R<EFBFBD><52><EFBFBD>סG
2026-05-19 13:56:59 +00:00
```yaml
Name: gateway
Host: 0.0.0.0
Port: 8888
Auth:
AccessSecret: ${JWT_ACCESS_SECRET}
AccessExpire: 900
RefreshAuth:
AccessSecret: ${JWT_REFRESH_SECRET}
AccessExpire: 604800
Zitadel:
Issuer: https://id.internal.example.com # self-hosted <20><><EFBFBD><EFBFBD>
2026-05-19 13:56:59 +00:00
ClientID: ${ZITADEL_CLIENT_ID}
2026-05-19 17:04:26 +00:00
JWKSUrl: https://id.internal.example.com/oauth/v2/keys
MgmtURL: https://id.internal.example.com/management/v1
2026-05-19 13:56:59 +00:00
MgmtToken: ${ZITADEL_MGMT_TOKEN}
EnforceAdminMFA: true # admin <20><> role<6C>]tenant_owner/tenant_admin/platform_super_admin<69>^<5E>j<EFBFBD><6A> TOTP
# Self-hosted<65>GLDAP IdP <20><> ZITADEL <20><><EFBFBD>s<EFBFBD><73><EFBFBD>~ AD/OpenLDAP
2026-05-19 17:04:26 +00:00
StepUp:
TokenSecret: ${JWT_STEPUP_SECRET}
TokenTTLSeconds: 300
AllowedActions:
- change_business_email
- change_business_phone
- delete_member
- tenant_admin_force_status
- revoke_all_sessions
Verification:
OTPLength: 6
OTPTTLSeconds: 300
ResendCooldownSeconds: 60
DailyLimit: 10
MaxAttempts: 5
TOTP:
Issuer: CloudEP # <20><><EFBFBD>ܦb Authenticator App <20>W<EFBFBD><57><EFBFBD>W<EFBFBD><57>
Algorithm: SHA1 # <20>ۮe Google Authenticator
2026-05-19 17:04:26 +00:00
Digits: 6
PeriodSeconds: 30
Window: 1 # <20>e<EFBFBD><65> <20><>1 <20><> 30s <20>϶<EFBFBD>
2026-05-19 17:04:26 +00:00
BackupCodeCount: 10
BackupCodeLength: 12 # hex chars
SecretKEK: ${TOTP_KEK} # AES-256 KEK<45>F<EFBFBD><46>ij<EFBFBD><C4B3> KMS / Vault
2026-05-19 17:04:26 +00:00
EnrollTTLSeconds: 600
Notification:
DefaultLocale: zh-tw
Async:
QueueRedisKey: notif:retry:zset
Worker: 4 # worker goroutine <20><>
2026-05-19 17:04:26 +00:00
MaxRetry: 5
BackoffSeconds: [1, 5, 30, 300, 1800]
RatePerTenant: # <20>C<EFBFBD><43><EFBFBD><EFBFBD><EFBFBD>q<EFBFBD>D<EFBFBD>t<EFBFBD>B<EFBFBD>]<5D><><EFBFBD>z<EFBFBD>o / <20><><EFBFBD>ݥΡ^
Email: 10000 # <20>C<EFBFBD><43>
2026-05-19 17:04:26 +00:00
SMS: 5000
Email:
Provider: sendgrid # sendgrid | ses | smtp
APIKey: ${SENDGRID_API_KEY}
From: noreply@example.com
Templates: # <20><><EFBFBD><EFBFBD> TemplateRegistry key <20><> provider template id
2026-05-19 17:04:26 +00:00
verify_email: d-xxxxxxxxxxxxx
step_up_email: d-yyyyyyyyyyyyy
account_suspended: d-zzzzzzzzzzzzz
tenant_welcome: d-aaaaaaaaaaaaa
SMS:
Provider: twilio # twilio | sns | smsapi
AccountSID: ${TWILIO_ACCOUNT_SID}
AuthToken: ${TWILIO_AUTH_TOKEN}
From: "+1234567890"
Templates:
verify_phone: "Your verification code is {code} (valid {expires_in}s)"
step_up_phone: "Step-up code: {code}"
Push:
Enabled: false # <20>w<EFBFBD>d
2026-05-19 17:04:26 +00:00
Webhook:
HMACSecret: ${NOTIF_WEBHOOK_HMAC}
2026-05-19 13:56:59 +00:00
Mongo:
# <20><> internal/library/mongo <20>]<5D>w
2026-05-19 13:56:59 +00:00
Redis:
Host: 127.0.0.1:6379
Type: node
Member:
DefaultLanguage: zh-tw
DefaultCurrency: TWD
Permission:
RBACModelPath: etc/rbac.conf
PolicySyncInterval: 5m
PolicyReloadChannel: casbin:reload # Redis Pub/Sub <20>q<EFBFBD>D<EFBFBD>]<5D>Y<EFBFBD>ɳq<C9B3><71><EFBFBD>A5m cron <20>©<EFBFBD><C2A9>^
2026-05-19 17:04:26 +00:00
PlatformAdminTenantID: ${PLATFORM_ADMIN_TENANT_ID}
PlatformAdminRoleKey: platform_super_admin
PlatformAdminAllowlistUIDs: ${PLATFORM_ADMIN_ALLOWLIST_UIDS} # break-glass <20>ΡA<CEA1><41><EFBFBD><EFBFBD> audit
2026-05-19 13:56:59 +00:00
CacheTTLSeconds: 300
2026-05-19 17:04:26 +00:00
DirectorySync:
MissingThreshold: 3
MaxChangeRatio: 0.20
DryRunOnFirstSync: true
DefaultWindow: 24h
AlertSink: ${OPS_WEBHOOK_URL}
AuditLog:
Sink: mongo # mongo | otel | dual
Mongo:
DB: gateway_audit # <20><>ij<EFBFBD>W<EFBFBD><57> DB instance / replica set
2026-05-19 17:04:26 +00:00
Collection: audit_logs
BatchSize: 100
FlushInterval: 1s
TTLDays: 90
OTEL:
Endpoint: ${OTEL_ENDPOINT} # Sink = otel / dual <20>ɥͮ<C9A5>
2026-05-19 17:04:26 +00:00
RateLimit:
Enabled: true
RedisPrefix: rl
WindowSeconds: 60
Rules:
- Match: /api/v1/auth/*
ByIP: 60 # 60 req / min / IP
ByUID: 30 # 30 req / min / UID<49>]<5D>w<EFBFBD>n<EFBFBD>J<EFBFBD>ɡ^
2026-05-19 17:04:26 +00:00
- Match: /api/v1/auth/step-up/*
ByUID: 10
- Match: /scim/v2/*
ByToken: 6000 # 6000 req / min / SCIM token<65>]<5D><> 100rps<70>^
2026-05-19 17:04:26 +00:00
- Match: /api/v1/*
ByUID: 600 # <20>@<40><> API <20>W<EFBFBD><57>
2026-05-19 17:04:26 +00:00
ByIP: 1200
2026-05-19 13:56:59 +00:00
```
---
## 18. <20><><EFBFBD>I<EFBFBD><49><EFBFBD><EFBFBD>
2026-05-19 13:56:59 +00:00
| <20><><EFBFBD>q | <20><><EFBFBD>e | <20><><EFBFBD>X |
2026-05-19 13:56:59 +00:00
|------|------|------|
| **P0** | <20>ؿ<EFBFBD><D8BF><EFBFBD><EFBFBD>[<5B>Bentity<74>Bredis key<65>Bconfig<69>B**`make seed-platform-admin` CLI**<2A>]<5D>ح<EFBFBD><D8AD><EFBFBD> platform admin uid + role<6C>^ | <20>i<EFBFBD>ҰʡB<CAA1>i<EFBFBD>s Mongo/Redis<69>A<EFBFBD><41><EFBFBD>x admin <20>i<EFBFBD>n<EFBFBD>J |
| **P1** | UID generator + ProvisioningUseCase<73>]OIDC/LDAP/SCIM <20>T<EFBFBD><54><EFBFBD><EFBFBD><EFBFBD>^+ token exchange | <20>i<EFBFBD>n<EFBFBD>J<EFBFBD><4A><EFBFBD>o JWT + <20>iŪ UID |
| **P2** | JWT middleware + jti <20>¦W<C2A6><57> + auth_gen + logout/refresh | <20><><EFBFBD><EFBFBD> Token <20>ͩR<CDA9>g<EFBFBD><67> |
| **P3** | Permission seed + PermissionTree + Casbin RBAC + Redis Adapter | <20>i LoadPolicy / Check |
| **P3.5** | Notification Module<6C>]<5D>Τ@<40>J<EFBFBD>f + Email/SMS Provider<65>^+ Verification + Step-up MFA + **TOTP** | <20>~<7E><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> + TOTP step-up + <20><><EFBFBD><EFBFBD><EFBFBD>I<EFBFBD>u<EFBFBD><75> |
| **P4** | member profile API + <20>w<EFBFBD>] Role seed + CasbinRBACMiddleware | `/members/me` + API <20><><EFBFBD>v<EFBFBD>ͮ<EFBFBD> |
| **P5** | RolePermission + UserRole + B2B Role CRUD + Permission <20>Ŀ<EFBFBD> API | <20><><EFBFBD><EFBFBD><E1A7B9><EFBFBD>۩w<DBA9>q |
| **P6** | Tenant <20>إ<EFBFBD> + ZITADEL CreateOrg + LDAP <20>]<5D>w | <20>h<EFBFBD><68><EFBFBD><EFBFBD> |
| **P7** | Directory Sync Worker<65>]AD + OpenLDAP<41>^+ <20><>10.4 guardrail | <20><><EFBFBD>~<7E>ؿ<EFBFBD><D8BF>P<EFBFBD>B<EFBFBD>]<5D>~<7E>P<EFBFBD>O<EFBFBD>@<40><><EFBFBD>ơ^ |
| **P8** | SCIM 2.0 endpoint + Group <20>M<EFBFBD>g | <20><><EFBFBD>~ provisioning |
| **P8.5** | Audit log sink<6E>]Mongo <20>W<EFBFBD><57> collection<6F>^+ Rate Limit middleware<72>]<5D><> <20><>20<32>^ | <20>i<EFBFBD>f<EFBFBD>p / <20><><EFBFBD>ݥ<EFBFBD> |
| **P9** | <20><><EFBFBD><EFBFBD><EFBFBD>]100 <20>U seed<65>^<5E>Bsharding<6E>B<EFBFBD><42><EFBFBD>u<EFBFBD>BJWT kid <20>h<EFBFBD><68><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> | <20>W<EFBFBD>u<EFBFBD>dz<EFBFBD> |
2026-05-19 17:04:26 +00:00
---
## 19. <20>w<EFBFBD>M<EFBFBD><4D><EFBFBD>ƶ<EFBFBD>
2026-05-19 17:04:26 +00:00
| # | ij<>D | **<EFBFBD>M<EFBFBD><EFBFBD>** | <20>]<5D>p<EFBFBD>v<EFBFBD>T |
2026-05-19 17:04:26 +00:00
|---|------|----------|----------|
| 1 | UID <20>榡 | **`{Prefix}-{Sequence}`**<2A>A<EFBFBD>p `AMEX-10000000` | <20><>12<31>FSequence <20>_<EFBFBD><5F> `10000000` |
| 2 | SCIM <20><><EFBFBD><EFBFBD> | **`/scim/v2/tenants/{tenant_id}/...`** | <20><>7.5<EFBFBD>B<EFBFBD><EFBFBD>10.3 |
| 3 | ZITADEL <20><><EFBFBD>p | **Self-hosted** | <20><>3.3<EFBFBD>FLDAP <20><><EFBFBD><EFBFBD>/VPN <20>s<EFBFBD>u |
| 4 | <20>v<EFBFBD><76><EFBFBD>ܧ<EFBFBD><DCA7>ͮ<EFBFBD> | **UserRole <20>ܧ<EFBFBD> `INCR auth_gen`<60>FRolePermission <20>ܧ<EFBFBD> reload policy + cache invalidate** | <20><>4.5<EFBFBD>B<EFBFBD><EFBFBD>6.11 |
| 5 | B2C <20><><EFBFBD><EFBFBD> | **<EFBFBD><EFBFBD>Ū seed <20>ҪO**<2A>A<EFBFBD><41><EFBFBD>i<EFBFBD>۩w<DBA9>q Role | <20><>6.12<EFBFBD>FB2C <20>T<EFBFBD><54> Role CRUD API |
| 6 | Refresh Token | **<EFBFBD><EFBFBD><EFBFBD><EFBFBD> + <20><> refresh jti <20>¦W<C2A6><57>** | <20><>4.5 Refresh <20><><EFBFBD><EFBFBD> |
| 7 | Casbin <20>h<EFBFBD><68><EFBFBD><EFBFBD><EFBFBD>j<EFBFBD><6A> | **policy <20>a `tenant_id` + immutable `role_key`** | <20><>6.7<EFBFBD>F<EFBFBD>קK<EFBFBD>P<EFBFBD>W role <20>󯲤<EFBFBD><F3AFB2A4>ìV |
| 8 | SCIM externalId | **<EFBFBD>O<EFBFBD>d<EFBFBD><EFBFBD><EFBFBD>Ȥ<EFBFBD><EFBFBD>ݥ~<7E><><EFBFBD>ѧO<D1A7>A<EFBFBD><41><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Gateway UID** | <20><>10.3<EFBFBD>FGateway UID <20>@<40><> SCIM id <20><> extension |
| 9 | Platform Admin bypass | **<EFBFBD><EFBFBD><EFBFBD>x role + allowlist<73>A<EFBFBD><41><EFBFBD><EFBFBD> audit** | <20><>6.7<EFBFBD>B<EFBFBD><EFBFBD>8.2<EFBFBD>F<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>b Casbin matcher |
| 10 | UIDPrefix | **<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>x<EFBFBD>ߤ@**<2A>]`tenants.uid_prefix` unique index<65>^ | <20><>12.2 |
| 11 | JWT Claims <20><><EFBFBD>e | **<EFBFBD><EFBFBD><EFBFBD><EFBFBD> role / permission <20>ַ<EFBFBD>**<2A>A<EFBFBD>C<EFBFBD><43><EFBFBD>d cache | <20><>4.3 |
| 12 | Refresh Token Reuse | **<EFBFBD><EFBFBD> refresh <20>G<EFBFBD><47><EFBFBD>ϥ<EFBFBD> = <20>s<EFBFBD><73> <20><> INCR auth_gen + audit** | <20><>4.5 |
| 13 | Token Exchange <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> | **id_token nonce SETNX + iat 5 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>f** | <20><>4.5 |
| 14 | Logout <20><><EFBFBD><EFBFBD> | **Issue <20><> redis <20>O access?refresh jti pair** | <20><>4.5 |
| 15 | RolePermission API <20>y<EFBFBD>N | **PUT <20><><EFBFBD>q<EFBFBD><71><EFBFBD>N** `{ permission_names: [...] }` + <20>j<EFBFBD><6A><EFBFBD>a tenant_id | <20><>6.8<EFBFBD>B<EFBFBD><EFBFBD>7.3<EFBFBD>B<EFBFBD><EFBFBD>9.3 |
| 16 | <20>~<7E><><EFBFBD>ӷ<EFBFBD> UserRole | **<EFBFBD><EFBFBD> source <20>j<EFBFBD><6A> Replace**<2A>Amanual <20>ä<EFBFBD><C3A4>Q<EFBFBD>~ | <20><>6.10 |
| 17 | PlainCode <20><><EFBFBD>@ | **Casbin <20>B<EFBFBD>~<7E>d `.plain_code` <20><><EFBFBD><EFBFBD>**<EFBFBD>A<EFBFBD>h role allow <20><><EFBFBD>G<EFBFBD><47> OR | <20><>6.9 |
| 18 | Permission.Name | **<EFBFBD>إ߫ᤣ<EFBFBD>i<EFBFBD><EFBFBD><EFBFBD>W**<2A>F<EFBFBD>o<EFBFBD><6F><EFBFBD><EFBFBD> `status=close` + <20>s<EFBFBD><73> | <20><>6.4 |
| 19 | <20><><EFBFBD>U<EFBFBD><55><EFBFBD>| | **<EFBFBD>w<EFBFBD>]**<2A><> ZITADEL Hosted UI<55>]B2C<32>^/ LDAP / SCIM<49>]B2B<32>^<5E>F**<2A>O<EFBFBD>d** platform-native usecase<73>]`LifecycleUseCase.CreateUnverified` + `OTPUseCase` + `Activate`<EFBFBD>^<5E>ѥ<EFBFBD><D1A5>Ӷ}<7D>q Gateway <20><><EFBFBD>͵<EFBFBD><CDB5>U<EFBFBD>]<5D>t email OTP <20><><EFBFBD>ҡ^ | <20><>3.4<EFBFBD>B<EFBFBD><EFBFBD>5.2.1<EFBFBD>B<EFBFBD><EFBFBD>5.9 |
| 20 | <20><><EFBFBD><EFBFBD> vs <20>~<7E><><EFBFBD><EFBFBD><EFBFBD>Ҥ<EFBFBD><D2A4>h | **ZITADEL <20>޵n<DEB5>J<EFBFBD><4A><EFBFBD><EFBFBD><EFBFBD>FGateway member <20><><EFBFBD><EFBFBD><EFBFBD>~<7E><> email / phone** | <20><>1.2<EFBFBD>B<EFBFBD><EFBFBD>5.4 |
| 21 | Step-up MFA | **<EFBFBD>ҥ<EFBFBD>**<2A>F<EFBFBD><46><EFBFBD><EFBFBD><EFBFBD>I action <20><> 5min <20><EFBFBD><E6A6B8> `step_up_token` | <20><>5.6<EFBFBD>B<EFBFBD><EFBFBD>9.6 |
| 22 | OTP <20><EFBFBD>q<EFBFBD>D | **<EFBFBD>۰e**<2A>]<5D>z<EFBFBD>L Notification Module <20>] Email / SMS Provider<65>^ | <20><>5.5<EFBFBD>B<EFBFBD><EFBFBD>11<EFBFBD>B<EFBFBD><EFBFBD>17 |
| 23 | MFA <20>j<EFBFBD><EFBFBD><EEB5A6> | **<EFBFBD><EFBFBD><EFBFBD>x<EFBFBD>j<EFBFBD><EFBFBD> admin role <20><> ZITADEL TOTP**<2A>F<EFBFBD>@<40><> user <20>w<EFBFBD>]<5D><><EFBFBD>j<EFBFBD><6A><EFBFBD>A<EFBFBD><41><EFBFBD><EFBFBD><EFBFBD>I<EFBFBD><49> Step-up | <20><>3.5 |
| 24 | KYC | **<EFBFBD><EFBFBD><EFBFBD>b<EFBFBD><EFBFBD>d<EFBFBD><EFBFBD>** | <20>X |
| 25 | <20>~<7E><> TOTP<54>]Authenticator App<70>^ | **<EFBFBD>ҥ<EFBFBD>**<2A>AGateway <20>ۦs AES-GCM <20>[<5B>K secret<65>F<EFBFBD>P ZITADEL <20><><EFBFBD><EFBFBD> TOTP <20>W<EFBFBD><57> | <20><>5.8 |
| 26 | Step-up <20>q<EFBFBD>D<EFBFBD>u<EFBFBD><75><EFBFBD><EFBFBD> | **TOTP > SMS > Email**<EFBFBD>FStart <20>ɨ<EFBFBD> enrolled <20><><EFBFBD>A<EFBFBD>D<EFBFBD>q<EFBFBD>D<EFBFBD>A<EFBFBD>i<EFBFBD><69> client `prefer_channel` <20>мg | <20><>5.6 |
| 27 | Notification Module | <20>W<EFBFBD><57> model <20>Ҳ<EFBFBD> `internal/model/notification/`<EFBFBD>A**<2A>Ҧ<EFBFBD> outbound <20>q<EFBFBD>T**<2A>Τ@<40><> `NotifierUseCase`<EFBFBD>Flibrary <20>h<EFBFBD><68> provider <20><> IO <20>ʸ<EFBFBD> | <20><>11 |
| 28 | OTP <20>J<EFBFBD>w<EFBFBD><77><EFBFBD><EFBFBD> | OTP / step-up <20><><EFBFBD>ӷP<D3B7><50><EFBFBD>e `DoNotPersistBody=true`<EFBFBD>Anotification <20><><EFBFBD><EFBFBD><EFBFBD>ȯd metadata<74>]target_hash<73>Bprovider_message_id<69>Bstatus<75>^ | <20><>11.3<EFBFBD>B<EFBFBD><EFBFBD>11.8 |
| 29 | UseCase <20><><EFBFBD>h | **Atomic primitives + Composite** <20><><EFBFBD>h<EFBFBD>G<EFBFBD><47><EFBFBD>l<EFBFBD>ʧ@<40>]Profile / Lifecycle / Provisioning / OTP / TOTP<54>^<5E>i<EFBFBD><69><EFBFBD>N<EFBFBD>զX<D5A6>FComposite<74>]Verification / StepUp<55>^<5E><><EFBFBD>`<60>βզX<D5A6>w<EFBFBD>ʸˡFlogic <20>i<EFBFBD><69><EFBFBD>ܸ<EFBFBD><DCB8>| | <20><>5.2 |
| 30 | OTP <20>]<5D>p | **Purpose-agnostic atomic primitive**<EFBFBD>G`OTPUseCase.Generate / Verify / Invalidate`<60>F`purpose` <20><><EFBFBD>ѥγ~<7E>]registration_email / business_email / step_up / ...<2E>^<5E>Acaller <20>ۭt<DBAD><EFBFBD>P<EFBFBD><50><EFBFBD><EFBFBD><EFBFBD>Ƨ@<40><> | <20><>5.2.1<EFBFBD>B<EFBFBD><EFBFBD>5.2.4<EFBFBD>B<EFBFBD><EFBFBD>5.9 |
| 31 | Provisioning <20><><EFBFBD><EFBFBD> | `EnsureMember` <20>**`EnsureFromOIDC` / `EnsureFromLDAP` / `EnsureFromSCIM`** <20>T<EFBFBD><54> atomic<69>F<EFBFBD><46><EFBFBD>P<EFBFBD>ӷ<EFBFBD><D3B7><EFBFBD><EFBFBD><EFBFBD><EFBFBD>޿褬<DEBF><E8A4AC><EFBFBD><EFBFBD><EFBFBD>X | <20><>5.2.1 |
| 32 | <20><><EFBFBD>x<EFBFBD><78><EFBFBD>U<EFBFBD><55><EFBFBD>A | <20>[<5B>^ `unverified` <20><><EFBFBD>A<EFBFBD>A**<2A><>** platform-native <20><><EFBFBD>|<7C>|<7C>X<EFBFBD>{<7B>FOIDC / LDAP / SCIM <20><><EFBFBD><EFBFBD> `active` | <20><>5.3 |
| A | SCIM `id` | **SCIM `id` = Gateway UID**<EFBFBD>]<5D><48>B<EFBFBD><42><EFBFBD>t<EFBFBD>Τ@<40>P<EFBFBD>^<5E>F`externalId` <20>d<EFBFBD><64><EFBFBD>Ȥ<EFBFBD><C8A4>ݡFZITADEL `sub` <20><> extension `urn:cloudep:scim:2.0:User:zitadelSub` | <20><>10.3 |
| B | Casbin <20>h pod <20>P<EFBFBD>B | **Redis Pub/Sub <20>Y<EFBFBD>ɳq<C9B3><71> + 5min cron <20><><EFBFBD>q reload <20>©<EFBFBD>**<EFBFBD>]<5D><><EFBFBD>O<EFBFBD>I<EFBFBD>Apod <20><><EFBFBD>Ҥ<EFBFBD><D2A4>|<7C>^ | <20><>6.11 |
| C | Tenant <20>إ߶<D8A5><DFB6><EFBFBD> | **Gateway <20><><EFBFBD><EFBFBD> tenant <20><><EFBFBD>Z<EFBFBD>]`status=provisioning`<60>^<5E><> <20>I<EFBFBD>s ZITADEL Mgmt <20><> Org <20><> <20>^<5E><> `org_id` <20><> `status=active`<60>F<EFBFBD><46><EFBFBD>Ѩ<EFBFBD><D1A8><EFBFBD><EFBFBD>v cron <20><><EFBFBD>թΤH<CEA4>u<EFBFBD><75> failed** | <20><>3.1<EFBFBD>B<EFBFBD><EFBFBD>7.4 |
| D | Platform Admin Bootstrap | **`make seed-platform-admin` CLI**<2A>]<5D>ح<EFBFBD><D8AD><EFBFBD> platform admin uid + role<6C>^<5E><><EFBFBD>D<EFBFBD>F`PLATFORM_ADMIN_ALLOWLIST_UIDS` <20><><EFBFBD><EFBFBD><EFBFBD>ܼƧ@ break-glass<73>A**<2A>j<EFBFBD><6A> audit** | <20><>18 P0 |
| E | Hybrid <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>y | **<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>æs**<2A>G`Member.Origin`<60>]<5D>D<EFBFBD>ӷ<EFBFBD><D3B7>Gzitadel_local / ldap / scim<69>^+ `UserRole.Source`<EFBFBD>]<5D>C<EFBFBD><43> role <20><><EFBFBD><EFBFBD><EFBFBD>ӷ<EFBFBD><D3B7>^<5E>Fsync replace <20><> source<63>B<EFBFBD><42>Ū<EFBFBD><C5AA><EFBFBD><EFBFBD><EFBFBD><EFBFBD> origin | <20><>3.2<EFBFBD>B<EFBFBD><EFBFBD>5<EFBFBD>B<EFBFBD><EFBFBD>6.10 |
| F | SCIM endpoint <20><><EFBFBD>v | **<EFBFBD>쪩 tenant <20><> SCIM Token <20><><EFBFBD>v**<2A>]read+write<74>^+ IP allowlist + rate limit + token rotation<6F>Fv2 <20>A<EFBFBD>[ `scim.users.write` / `scim.groups.write` scope | <20><>7.5 |
| G | Audit log sink | **<EFBFBD>W<EFBFBD><EFBFBD> Mongo `audit_logs` collection**<2A>]<5D><>ij<EFBFBD>W<EFBFBD><57> DB instance <20>οW<CEBF><57> replica set<65>^+ TTL 90 <20><> + <20><><EFBFBD>B batch flush<73>F<EFBFBD><46><EFBFBD><EFBFBD><EFBFBD>I<EFBFBD>ƥ<EFBFBD><C6A5>P<EFBFBD>B<EFBFBD>g<EFBFBD>F<EFBFBD>i<EFBFBD><69> OTEL log <20><><EFBFBD>g<EFBFBD>k<EFBFBD><6B> | <20><>4.5<EFBFBD>B<EFBFBD><EFBFBD>8.2<EFBFBD>B<EFBFBD><EFBFBD>20 |
| H | <20>b<EFBFBD><62><EFBFBD>R<EFBFBD><52><EFBFBD><EFBFBD><EFBFBD><EFBFBD> | **<EFBFBD>n<EFBFBD>R 30 <20>ѫ<EFBFBD><D1AB>ΦW<CEA6><57>**<2A>G<EFBFBD>ߧY `status=deleted` + <20>M<EFBFBD>P token + ZITADEL disable<6C>F30 <20><> cron <20>ΦW<CEA6><57> PII <20><><EFBFBD><EFBFBD><EFBFBD>]email/phone/displayName/avatar/zitadel_sub/business_*<2A>^<5E>F<EFBFBD>O<EFBFBD>d uid/tenant_id/timestamps/audit <20>s<EFBFBD><73><EFBFBD><EFBFBD> | <20><>5.3<EFBFBD>B<EFBFBD><EFBFBD>5.7 |
| I | Member <20><><EFBFBD><EFBFBD> SoT | **<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>**<2A>G<EFBFBD><47><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]zitadel_sub<75>BIdP email/name<6D>BZITADEL status<75>^<5E><> ZITADEL <20><><EFBFBD>ǡF<C7A1>~<7E><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]business_email/phone<6E>Blanguage<67>Bcurrency<63>Bavatar<61>^<5E><> Gateway <20><><EFBFBD>ǡFprovisioning <20><><EFBFBD><EFBFBD><EFBFBD>]external_id<69>Bldap_dn<64>^<5E><> <20>ӷ<EFBFBD><D3B7>t<EFBFBD>ά<EFBFBD><CEAC><EFBFBD> | <20><>5<EFBFBD>B<EFBFBD><42>9.1 |
| J | Directory Sync <20>~<7E>P<EFBFBD>O<EFBFBD>@ | **<EFBFBD>s 3 <20><><EFBFBD>]<5D>s<EFBFBD><73> 3 <20>ѡ^<5E><EFBFBD><E4A4A3><EFBFBD>~ suspend**<2A>B<EFBFBD>榸 sync <20><><EFBFBD><EFBFBD> > 20% <20>۰<EFBFBD><DBB0><EFBFBD> dry-run + <20><69>B<EFBFBD><42><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>p<EFBFBD>j<EFBFBD><6A> dry-run<75>B<EFBFBD>R<EFBFBD><52><EFBFBD><EFBFBD> cron <20>q<EFBFBD>L<EFBFBD><4C><EFBFBD><EFBFBD> guardrail | <20><>10.4 |
| K | Rate Limiting | **go-zero middleware + Redis sliding-window <20>h<EFBFBD><68>**<EFBFBD>GIP / UID / TenantSCIMToken <20>T<EFBFBD>h<EFBFBD>F`/auth/*` <20>C IP 60rpm + <20>C UID 30rpm<70>F`/scim/*` <20>C token 100rps<70>F<EFBFBD>@<40><> API <20>C UID 600rpm<70>FOTP <20><> <20><>5.5 <20>J<EFBFBD><4A><EFBFBD>N<EFBFBD>o | <20><>17 RateLimit<69>B<EFBFBD><42>20 |
| L | JWT Secret Rotation | **<EFBFBD>`kid` header + <20>h key <20>æs**<2A>GAccess / Refresh / Step-up <20>U<EFBFBD>ۿW<DBBF><57> key set<65><46>o<EFBFBD>γ̷s kid<69>A<EFBFBD><41><EFBFBD>Ҩ<EFBFBD> active kid <20>W<EFBFBD><57><EFBFBD>F<EFBFBD><46><EFBFBD><EFBFBD><EFBFBD>y<EFBFBD>{<7B>G<EFBFBD>o<EFBFBD>s kid <20><> <20>s token <20>ηs kid <20><> <20><><EFBFBD><EFBFBD> token expire <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> kid | <20><>4.4 |
2026-05-19 13:56:59 +00:00
---
## 20. Audit Log <20>P Rate Limit
2026-05-19 13:56:59 +00:00
2026-05-19 17:04:26 +00:00
### 20.1 Audit Log
**Sink<6E>]<5D>w<EFBFBD>M<EFBFBD><4D><EFBFBD>^**<2A>G<EFBFBD>W<EFBFBD><57> Mongo `audit_logs` collection<6F>]<5D><>ij**<2A>W<EFBFBD><57> DB instance** <20><> replica set<65>A<EFBFBD>קK OLTP <20><><EFBFBD><EFBFBD><EFBFBD>^<5E>C
2026-05-19 17:04:26 +00:00
```go
type AuditLog struct {
ID primitive.ObjectID
TenantID string
Action string // member.created | role.assigned | step_up.confirmed ...
Actor Actor // {uid, role_keys, ip, ua, jti}
Target Target // {kind: member|role|tenant, id, before, after}
Severity enum.Severity // info | warn | critical
Result enum.Result // success | denied | error
Reason string // <20><><EFBFBD>ѭ<EFBFBD><D1AD>] / denied <20>z<EFBFBD><7A>
Metadata bson.M // <20>ʺA<CABA><41><EFBFBD><EFBFBD><EFBFBD>A<EFBFBD>p step_up_jti<74>Bscim_op<6F>Bsource
2026-05-19 17:04:26 +00:00
OccurredAt int64 // epoch ms
}
```
**<2A>g<EFBFBD>J<EFBFBD><4A><EFBFBD><EFBFBD><EFBFBD>G**
2026-05-19 17:04:26 +00:00
| Severity | <20>Ҧ<EFBFBD> | <20><><EFBFBD>ѳB<D1B3>z |
2026-05-19 17:04:26 +00:00
|----------|------|---------|
| `critical`<EFBFBD>]<5D><><EFBFBD>v<EFBFBD>B<EFBFBD>R<EFBFBD><52><EFBFBD>Bstep-up<75>BPlatform Admin bypass<73>B<EFBFBD>v<EFBFBD><76><EFBFBD>M<EFBFBD>P<EFBFBD>^ | **<EFBFBD>P<EFBFBD>B**<2A>g<EFBFBD>J<EFBFBD>F<EFBFBD>g<EFBFBD><67><EFBFBD>ѫh<D1AB><68><EFBFBD>ӷ~<7E>Ⱦާ@<40>^<5E>u | <20>ڵ<EFBFBD><DAB5>ШD<D0A8>A<EFBFBD>קK<D7A7>L<EFBFBD><4C><EFBFBD><EFBFBD><EFBFBD>q<EFBFBD>L |
| `info`<EFBFBD><><C5AA><EFBFBD>B<EFBFBD>v<EFBFBD><76><EFBFBD>q<EFBFBD>L<EFBFBD>^ | **<EFBFBD><EFBFBD><EFBFBD>B**<2A>Gbuffered channel <20><> batch insert<72>]`BatchSize=100`<60>B`FlushInterval=1s`<60>^ | drop + metrics<63>]<5D><69>A<EFBFBD><41><EFBFBD><EFBFBD><EFBFBD>v<EFBFBD>T<EFBFBD>~<7E>ȡ^ |
2026-05-19 17:04:26 +00:00
- TTL index<65>G`{ OccurredAt: 1 }` TTL 90 <20>ѡF<D1A1>W<EFBFBD>L<EFBFBD>h<EFBFBD>k<EFBFBD>ɡ]<5D>i<EFBFBD><69> OTEL log <20><><EFBFBD>g<EFBFBD>O<EFBFBD>d<EFBFBD><64><EFBFBD>[<5B>^
- Index<65>G`{ TenantID: 1, OccurredAt: -1 }`<60>B`{ TenantID: 1, Actor.uid: 1, OccurredAt: -1 }`<60>B`{ TenantID: 1, Action: 1, OccurredAt: -1 }`
- **<2A>ΦW<CEA6>Ƥ<EFBFBD><C6A4>v<EFBFBD>T audit**<2A>Gactor / target uid <20><><EFBFBD>O<EFBFBD>d<EFBFBD>]<5D>Y<EFBFBD><59> member <20>w<EFBFBD>ΦW<CEA6>ơ^<5E>A<EFBFBD>F<EFBFBD><46><EFBFBD>u<EFBFBD>̤֥<CCA4><D6A5>n PII + <20>s<EFBFBD><73><EFBFBD>ʡv
2026-05-19 17:04:26 +00:00
### 20.2 Rate Limit
**<2A>޳N<DEB3><EFBFBD>]<5D>w<EFBFBD>M<EFBFBD><4D><EFBFBD>^**<2A>Ggo-zero middleware<72>]<5D>ۻs / <20>l<EFBFBD>͡^+ Redis sliding-window<6F>C
2026-05-19 17:04:26 +00:00
```
Key: rl:{dimension}:{key}:{path_pattern} # dimension = ip | uid | scim_token
Value: ZSET<45>]timestamp_ms : nonce<63>^TTL = WindowSeconds
2026-05-19 17:04:26 +00:00
```
**<2A>t<EFBFBD><74><EFBFBD>k**<2A>G
2026-05-19 17:04:26 +00:00
```
1. now := time.Now().UnixMilli()
2. ZREMRANGEBYSCORE rl:... 0 (now - window_ms)
3. count := ZCARD rl:...
4. if count >= limit <20><> 429 + Retry-After
2026-05-19 17:04:26 +00:00
5. ZADD rl:... now {random}
6. EXPIRE rl:... window
```
**<2A><><EFBFBD>h<EFBFBD>R<EFBFBD><52><EFBFBD>W<EFBFBD>h<EFBFBD>]<5D><><EFBFBD>Ǥǰt<C7B0>^<5E>G**
2026-05-19 17:04:26 +00:00
| <20><><EFBFBD>| | <20><><EFBFBD><EFBFBD> | <20>W<EFBFBD><57> |
2026-05-19 17:04:26 +00:00
|------|------|------|
| `/api/v1/auth/step-up/*` | UID | 10 req/min |
| `/api/v1/auth/*` | IP / UID | 60 / 30 req/min |
| `/scim/v2/*` | SCIM token | 6000 req/min<69>]<5D><> 100rps<70>^ |
| `/api/v1/*`<EFBFBD>]<5D><><EFBFBD>l<EFBFBD>^ | UID / IP | 600 / 1200 req/min |
2026-05-19 17:04:26 +00:00
- **<2A><><EFBFBD>} endpoint**<2A>]exchange / refresh<73>^<5E>H IP <20><><EFBFBD>D<EFBFBD>BUID <20><><EFBFBD><EFBFBD><EFBFBD>]<5D><><EFBFBD>n<EFBFBD>J<EFBFBD>ɵL UID<49>^
- <20>R<EFBFBD><52><EFBFBD><EFBFBD><EFBFBD>^ `429` + `Retry-After: {seconds}` + `X-RateLimit-Remaining`
- OTP / <20>~<7E><><EFBFBD><EFBFBD><EFBFBD>Ҩ<EFBFBD> <20><>5.5 <20><> `verify:rate` / `verify:daily`<EFBFBD>A**<2A><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>**<2A>g RateLimit middleware<72>]<5D>קK<D7A7>N<EFBFBD>o<EFBFBD>Q<EFBFBD><51><EFBFBD>ӡ^
- <20>]<5D>w<EFBFBD><77> <20><>17 `RateLimit`
2026-05-19 17:04:26 +00:00
---
## <20><><EFBFBD><EFBFBD> A<>G<EFBFBD>P model.md <20><><EFBFBD><EFBFBD><EFBFBD>Y
2026-05-19 17:04:26 +00:00
- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>G**<2A><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>**<2A>]<5D>[<5B>c<EFBFBD>B<EFBFBD>y<EFBFBD>{<7B>BAPI<50>B<EFBFBD>v<EFBFBD><76><EFBFBD>ҫ<EFBFBD><D2AB>^
- [model.md](./model.md)<29>G**<2A><><EFBFBD><EFBFBD><EFBFBD>g**<2A>]entity / repository / usecase <20>{<7B><><EFBFBD>X<EFBFBD>W<EFBFBD>d<EFBFBD>^
2026-05-19 17:04:26 +00:00
<EFBFBD><EFBFBD><EFBFBD>@<40>ɨ<EFBFBD><C9A8><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>f<EFBFBD>t<EFBFBD>ϥΡC
2026-05-19 13:56:59 +00:00
---
## <20><><EFBFBD><EFBFBD> B<>GServiceContext <20>ո˯<D5B8><CBAF><EFBFBD>
2026-05-19 13:56:59 +00:00
```go
type ServiceContext struct {
Config config.Config
Validator validate.Validate
// library clients<74>]<5D><> IO<49>A<EFBFBD>º<EFBFBD><C2BA>ʸ˥~<7E><> SDK<44>^
2026-05-19 17:04:26 +00:00
Zitadel *zitadel.Client
EmailSender libemail.Sender
SMSSender libsms.Sender
SecretCipher libcrypto.Cipher // TOTP secret <20>[<5B>ѱK
2026-05-19 17:04:26 +00:00
TOTPGen libtotp.Generator
2026-05-19 13:56:59 +00:00
// usecases
AuthUC authusecase.TokenUseCase
2026-05-19 17:04:26 +00:00
StepUpTokenUC authusecase.StepUpTokenUseCase
2026-05-19 13:56:59 +00:00
MemberProvUC memberusecase.ProvisioningUseCase
MemberProfileUC memberusecase.ProfileUseCase
MemberAdminUC memberusecase.AdminUseCase
2026-05-19 17:04:26 +00:00
VerificationUC memberusecase.VerificationUseCase
StepUpUC memberusecase.StepUpUseCase
TOTPUC memberusecase.TOTPUseCase
2026-05-19 13:56:59 +00:00
TenantUC memberusecase.TenantUseCase
ScimUC memberusecase.ScimUseCase
2026-05-19 17:04:26 +00:00
// notification module
NotifierUC notifusecase.NotifierUseCase
// permission usecases<65>]<5D><><EFBFBD><EFBFBD> permission-server <20><><EFBFBD><EFBFBD><EFBFBD>^
2026-05-19 13:56:59 +00:00
PermRBACUC permusecase.RBACUseCase
PermUC permusecase.PermissionUseCase
RoleUC permusecase.RoleUseCase
RolePermUC permusecase.RolePermissionUseCase
UserRoleUC permusecase.UserRoleUseCase
RoleMappingUC permusecase.RoleMappingUseCase
AuthQueryUC permusecase.AuthorizationQueryUseCase
}
```
---
## <20><><EFBFBD><EFBFBD> C<>Gpermission-server <20>E<EFBFBD><45><EFBFBD><EFBFBD><EFBFBD>ӡ]<5D>{<7B><><EFBFBD>X<EFBFBD>š^
2026-05-19 13:56:59 +00:00
2026-05-19 17:04:26 +00:00
| permission-server <20>ɮ<EFBFBD> | Gateway <20>ؼ<EFBFBD> | <20>E<EFBFBD><45><EFBFBD>覡 |
2026-05-19 13:56:59 +00:00
|------------------------|--------------|----------|
| `pkg/usecase/permission_tree.go` | `model/permission/usecase/permission_tree.go` | <20>X<EFBFBD>G<EFBFBD><47><EFBFBD>˷h<CBB7><68> |
| `pkg/usecase/casbin_redis_rbac.go` | `model/permission/usecase/rbac.go` | <20>[ `tenant_id` + `role_key` <20><><EFBFBD><EFBFBD> |
| `pkg/repository/casbin_redis_adapter.go` | `model/permission/repository/casbin_redis_adapter.go` | <20>אּ tenant-scoped policy key |
| `pkg/domain/rbac/rule.go` | `model/permission/rbac/rule.go` | <20><><EFBFBD>˷h<CBB7><68> |
| `etc/rbac.conf` | `etc/rbac.conf` | <20>[<5B>J tenant request / policy <20><><EFBFBD><EFBFBD> |
| `pkg/usecase/role.go` | `model/permission/usecase/role.go` | `ClientID`<EFBFBD><EFBFBD>`TenantID` |
| `pkg/usecase/role_permission.go` | `model/permission/usecase/role_permission.go` | <20>[ `tenant_id` <20><><EFBFBD>b<EFBFBD>P<EFBFBD>d<EFBFBD>ߺ<EFBFBD><DFBA><EFBFBD> |
| `pkg/usecase/user_role.go` | `model/permission/usecase/user_role.go` | <20><><EFBFBD><EFBFBD>h<EFBFBD><68><EFBFBD><EFBFBD> |
| `pkg/usecase/token.go` | **`model/auth/usecase/token.go`** | <20><><EFBFBD>b permission <20>Ҳ<EFBFBD> |
| `generate/database/seeders/*_permission*` | `generate/database/seeders/` <20><> Mongo seed | <20>אּ Gateway seed job |
2026-05-19 13:56:59 +00:00
---
## <20>׭q<D7AD><71><EFBFBD><EFBFBD>
2026-05-19 13:56:59 +00:00
| <20><><EFBFBD><EFBFBD> | <20><><EFBFBD><EFBFBD> | <20><><EFBFBD><EFBFBD> |
2026-05-19 13:56:59 +00:00
|------|------|------|
| 2026-05-19 | 0.1.0 | <20><><EFBFBD>Z<EFBFBD>Gauth + member + permission<6F>]B2B <20>۩w<DBA9>q<EFBFBD>^+ ZITADEL/LDAP/SCIM |
| 2026-05-19 | 0.2.0 | <20><><EFBFBD><EFBFBD> app-cloudep-permission-server<65>GCasbin RBAC<41>BPermission Tree<65>BRole/RolePermission |
| 2026-05-19 | 0.3.0 | <20>w<EFBFBD>w<EFBFBD><77> <20><>19<31>]1<>V6<56>^<5E>GUID <20>e<EFBFBD><65><EFBFBD><EFBFBD>BSCIM tenant_id <20><><EFBFBD>ѡBZITADEL self-hosted<65>Bauth_gen <20>j<EFBFBD><6A><EFBFBD><EFBFBD><EFBFBD>s<EFBFBD>BB2C <20><>Ū<EFBFBD>BRefresh <20><><EFBFBD><EFBFBD> |
| 2026-05-19 | 0.4.0 | <20>ɱj<C9B1>h<EFBFBD><68><EFBFBD><EFBFBD> Casbin<69>Bimmutable Role.Key<65>BSCIM externalId<49>BPlatform Admin bypass <20>P<EFBFBD>v<EFBFBD><76><EFBFBD>ͮĵ<CDAE><C4B5><EFBFBD> |
| 2026-05-20 | 0.5.0 | Best-practice <20><><EFBFBD>ġGJWT <20><><EFBFBD><EFBFBD> role <20>ַӡBRefresh Reuse Detection<6F>BToken Exchange Nonce<63>BLogout pair<69>BRolePermission tenant <20><><EFBFBD>b + PUT <20><><EFBFBD>q<EFBFBD><71><EFBFBD>N<EFBFBD>B<EFBFBD>~<7E><><EFBFBD>ӷ<EFBFBD> source <20>j<EFBFBD><6A><EFBFBD>BPlainCode <20>E<EFBFBD>X<EFBFBD>BPermission.Name <20><><EFBFBD>i<EFBFBD><69><EFBFBD>BUIDPrefix <20><><EFBFBD><EFBFBD><EFBFBD>x<EFBFBD>ߤ@<40>BRole.Key <20>W<EFBFBD>h<EFBFBD>B<EFBFBD><42><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƭ<EFBFBD> A<><41>B<EFBFBD><42>C |
| 2026-05-20 | 0.6.0 | <20>ɤJ<C9A4>~<7E><><EFBFBD><EFBFBD><EFBFBD>Ҥ<EFBFBD><D2A4>h<EFBFBD>GGateway <20><><EFBFBD><EFBFBD><EFBFBD>ѵ<EFBFBD><D1B5>U API<50>]<5D><>3.4<EFBFBD>^<5E>F<EFBFBD>s<EFBFBD>W<EFBFBD>~<7E><> Email / Phone <20><><EFBFBD><EFBFBD><EFBFBD>]<5D><>5.4<EFBFBD>B<EFBFBD><EFBFBD>9.5<EFBFBD>^<5E>FStep-up MFA <20>ҥΡ]<5D><>5.6<EFBFBD>B<EFBFBD><EFBFBD>9.6<EFBFBD>^<5E>FOTP <20>۰e Email + SMS Provider<65>]<5D><>5.5<EFBFBD>B<EFBFBD><EFBFBD>17 Notification<6F>^<5E>F<EFBFBD><46><EFBFBD>x admin <20>j<EFBFBD><6A> ZITADEL TOTP<54>]<5D><>3.5<EFBFBD>^<5E>F<EFBFBD>s<EFBFBD>W<EFBFBD><57><EFBFBD><EFBFBD> Redis key<65>BAPI<50>B<EFBFBD>]<5D>w<EFBFBD>B<EFBFBD>M<EFBFBD><4D><EFBFBD>C 19<31>V24 |
| 2026-05-20 | 0.7.0 | <20>ݨM<DDA8><4D> A<>VL <20><><EFBFBD>Ʃ<EFBFBD><C6A9>O<EFBFBD>GSCIM id = Gateway UID + ZITADEL sub extension<6F>]<5D><>10.3<EFBFBD>^<5E>FCasbin <20>h pod Pub/Sub + 5min cron <20>©<EFBFBD><C2A9>]<5D><>6.11<EFBFBD>^<5E>FTenant <20>إ<EFBFBD> saga<67>]<5D><>3.1<EFBFBD>^<5E>FPlatform Admin seed CLI<4C>]<5D><>18 P0<50>^<5E>FMember.Origin + UserRole.Source <20><><EFBFBD><EFBFBD><EFBFBD>]<5D><>5.4<EFBFBD>B<EFBFBD><EFBFBD>6.10<EFBFBD>^<5E>FSCIM token <20><><EFBFBD>v + IP allowlist<73>]<5D><>7.5<EFBFBD>^<5E>F<EFBFBD>W<EFBFBD><57> audit_logs collection + TTL 90d<30>]<5D><>20.1<EFBFBD>^<5E>F<EFBFBD>n<EFBFBD>R 30 <20>ѰΦW<CEA6>ơ]<5D><>5.7<EFBFBD>^<5E>F<EFBFBD><46><EFBFBD><EFBFBD><EFBFBD><EFBFBD> SoT<6F>]<5D><>5.3<EFBFBD>^<5E>FDirectory Sync guardrail<69>]<5D><>10.4<EFBFBD>^<5E>FRedis sliding-window rate limit<69>]<5D><>20.2<EFBFBD>^<5E>FJWT kid <20>h key <20>æs<C3A6>]<5D><>4.4<EFBFBD>^ |
| 2026-05-20 | 0.8.0 | <20><><EFBFBD>X<EFBFBD>W<EFBFBD><57> **Notification Module**<EFBFBD>]<5D><>11<31>^<5E>G<EFBFBD>Ҧ<EFBFBD> outbound <20>q<EFBFBD>T<EFBFBD>Τ@<40>J<EFBFBD>f<EFBFBD>B<EFBFBD>t idempotency / <20><><EFBFBD><EFBFBD> / DLQ / <20>ҪO / <20>h<EFBFBD>y<EFBFBD>B<EFBFBD>ӷP<D3B7><50><EFBFBD>e `DoNotPersistBody`<EFBFBD>F<EFBFBD>s<EFBFBD>W **<EFBFBD>~<7E><> TOTP**<2A>]<5D><>5.8<EFBFBD>^<5E>䴩 Google Authenticator<6F>A<EFBFBD>P ZITADEL <20><><EFBFBD><EFBFBD> TOTP <20>W<EFBFBD>ߡFstep-up <20>q<EFBFBD>D<EFBFBD>u<EFBFBD><75><EFBFBD>ǧאּ **TOTP > SMS > Email**<EFBFBD>]<5D><>5.6<EFBFBD>^<5E>F<EFBFBD>ؿ<EFBFBD><D8BF>BServiceContext<78>BMongo collections<6E>BRedis key<65>B<EFBFBD>]<5D>w<EFBFBD>ɡB<C9A1><42><EFBFBD>I<EFBFBD><49><EFBFBD>ǡB<C7A1>M<EFBFBD><4D><EFBFBD>C 25<32>V28 <20>P<EFBFBD>B<EFBFBD><42><EFBFBD>s<EFBFBD>F<EFBFBD><46>11<31>V<EFBFBD><56>19 <20><><EFBFBD>`<60>s<EFBFBD><73><EFBFBD><EFBFBD><EFBFBD><EFBFBD> +1 |
| 2026-05-20 | 0.9.0 | **UseCase <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]<5D>~<7E><><EFBFBD>޿<EFBFBD><DEBF>Ȥ<EFBFBD><C8A4><EFBFBD><EFBFBD>@<40>^**<EFBFBD>G<EFBFBD><EFBFBD>5.2 <20><><EFBFBD>g<EFBFBD><67> Atomic primitives + Composite <20><><EFBFBD>h<EFBFBD>F<EFBFBD>s<EFBFBD>W `OTPUseCase`<EFBFBD>]purpose-agnostic atomic<69>^<5E>B`LifecycleUseCase`<60>]CreateUnverified / Activate / Suspend / Reactivate / SoftDelete / AbortPending<6E>^<5E>F`ProvisioningUseCase` <20><> `EnsureFromOIDC / LDAP / SCIM` <20>T<EFBFBD><54><EFBFBD><EFBFBD><EFBFBD>F`ProfileUseCase` <20>[ `SetBusinessEmailVerified` / `SetBusinessPhoneVerified` atomic<69>F<EFBFBD>[<5B>^ `unverified` <20><><EFBFBD>A<EFBFBD>]<5D><> platform-native <20><><EFBFBD>|<7C>^<5E>F<EFBFBD>ɧ<EFBFBD> Member entity <20><><EFBFBD><EFBFBD><EFBFBD>BEnum <20><><EFBFBD>סBRequest DTO<54>F<EFBFBD>s<EFBFBD>W <20><>5.9 <20>s<EFBFBD>ƥܨҡ]5 case<73>^<5E>F<EFBFBD><46>14 OTP Redis key <20><> purpose-based<65>F<EFBFBD>M<EFBFBD><4D><EFBFBD>C 19 <20>ץ<EFBFBD><D7A5>B<EFBFBD>s<EFBFBD>W 29<32>V32 |