Auth system — Keycloak + OIDC + RBAC/ABAC + MFA + step-up
Modules :
platform/auth-svc(façade Keycloak Admin + endpoints/me,/sessions,/step-up),shared/auth-client-jvm(lib JWT validation + Authz pure functions partagée par tous services backend), Keycloak 25.x (déployé séparément).ADRs : ADR-002, ADR-009, ADR-029, ADR-032, ADR-033.
Repo : vitakyc-platform —
platform/auth-svc/,shared/auth-client-jvm/,infra/keycloak/realm-template/.
Cette page fixe la spec d’ingénierie complète du système auth VitaKYC. Un développeur qui la lit doit pouvoir construire auth-svc + intégrer auth-client-jvm dans un nouveau service backend sans question résiduelle :
- Architecture Keycloak realms + clients + flows
- Modèle JWT claims + lib validation
- Permissions catalog + RBAC/ABAC pure Kotlin
- MFA TOTP + WebAuthn enrollment + verification
- Step-up MFA pour actions sensibles
- Federation OIDC/SAML tenant tier-1
- Audit signed events
- API REST
auth-svc+ frontend OIDC integration
1. Vue d’ensemble
Section intitulée « 1. Vue d’ensemble »Flow nominal login agent :
- Agent ouvre back-office (
https://kyc.banque.tn/admin) → redirect OIDC Authorization Code + PKCE vers Keycloak - Keycloak affiche login (themed brand tenant) → password + TOTP MFA → JWT signed RS256
- Frontend stocke access (15 min) + refresh (8h, rotation à chaque usage) en httpOnly cookie
- À chaque requête API :
Authorization: Bearer <jwt>→ service backend valide JWT via JWKS cached + parseVitaKycPrincipal - Authz pure functions enforcent RBAC/ABAC (
Authz.require(principal, Permission.CASES_DECIDE)) - Pour action sensible (publish policy, 4-eyes confirm) : middleware exige
step_up_untilactif → sinon redirect versPOST /step-up/init(re-MFA) - Tous les events Keycloak (login, MFA enroll, role change) → Kafka
auth.audit.v1→audit-svcsigne Ed25519 + chaîne hash + Postgres append-only
2. Realms strategy
Section intitulée « 2. Realms strategy »2.1 Realm vitakyc-saas — fintechs et tenants light
Section intitulée « 2.1 Realm vitakyc-saas — fintechs et tenants light »Single realm partagé pour tous les tenants qui n’ont pas d’IdP existant.
| Item | Valeur |
|---|---|
| Nom | vitakyc-saas |
| Login URL | https://auth.vitakyc.io/realms/vitakyc-saas/protocol/openid-connect/auth |
| Multi-tenant | via attribut user tenantId injecté dans JWT claims |
| MFA | TOTP obligatoire — WebAuthn opt-in |
| Brute force | 10 attempts → 15 min lockout |
| Sessions | 8h SSO, 30 min idle |
| Branding | thème par tenant via header X-Tenant-Hint ou paramètre URL ?tenant_hint=TN-BANQUEX |
2.2 Realm dédié par banque tier-1
Section intitulée « 2.2 Realm dédié par banque tier-1 »Banque tier-1 (BTE, ATB, Amen) a son AD/Azure AD existant avec contraintes propres (policy mots de passe, MFA hardware token, certs CA privés). Realm dédié :
| Item | Valeur |
|---|---|
| Nom | <tenant_slug> (ex tn-banquex, tn-bte) |
| Federation | OIDC ou SAML 2.0 broker vers IdP banque |
| MFA | inherit IdP banque OR Keycloak MFA forcé en surcouche |
| Branding | full custom realm theme (logo, palette, copyright) |
| Provisioning | SCIM 2.0 (V1) ou import CSV (V0) |
2.3 Sélection du realm
Section intitulée « 2.3 Sélection du realm »// auth-svc resolves realm from request contextfun resolveRealm(ctx: RequestContext): String = when { ctx.host.endsWith(".vitakyc.io") -> "vitakyc-saas" ctx.host == "kyc.bte.com.tn" -> "tn-bte" ctx.host == "kyc.banquex.tn" -> "tn-banquex" else -> "vitakyc-saas"}Mapping host → realm géré par config tenant en Postgres tenant_config.auth_realm.
3. JWT claims standardisés
Section intitulée « 3. JWT claims standardisés »{ "iss": "https://auth.vitakyc.io/realms/tn-banquex", "aud": ["vitakyc-back-office", "vitakyc-services"], "sub": "user_8a7f3c1e9d4b2a6f", "session_id": "sess_xxx", "tenant_id": "TN-BANQUEX", "tenant_realm": "tn-banquex", "roles": ["agent_l2", "auditor"], "permissions": [ "cases:read", "cases:claim", "cases:decide", "cases:confirm", "audit:read", "audit:replay" ], "skills": ["RETAIL", "PEP", "FATCA"], "seniority": "L2", "mfa_amr": ["pwd", "totp"], "mfa_acr": "urn:mace:incommon:iap:silver", "step_up_until": null, "iat": 1745000000, "exp": 1745000900, "preferred_username": "leila.bensaid", "email": "leila.bensaid@banquex.tn", "email_verified": true, "name": "Leïla Ben Saïd"}Mapping NIST AAL :
| AAL | mfa_acr | Authenticators required |
|---|---|---|
| AAL1 | urn:mace:incommon:iap:bronze | password seul |
| AAL2 | urn:mace:incommon:iap:silver | password + TOTP / push mobile |
| AAL3 | urn:mace:incommon:iap:gold | password + WebAuthn (FIDO2 hardware) |
Politique :
- agent_l1 : AAL2 minimum
- agent_l2 / mlro / compliance_officer : AAL2 minimum, AAL3 recommandé
- dsi_admin / auditor : AAL3 obligatoire
- Step-up : élève temporairement AAL pour 15 min (logged dans
step_up_until)
4. Permissions catalog (RBAC + ABAC)
Section intitulée « 4. Permissions catalog (RBAC + ABAC) »4.1 Catalogue exhaustif (16 permissions V0, étendable)
Section intitulée « 4.1 Catalogue exhaustif (16 permissions V0, étendable) »| Permission | Resource | Action | Roles par défaut |
|---|---|---|---|
cases:read | cases | read | agent_l1, agent_l2, mlro, auditor, dsi_admin |
cases:claim | cases | claim | agent_l1, agent_l2, mlro |
cases:decide | cases | decide | agent_l1, agent_l2, mlro |
cases:confirm | cases | confirm (4-eyes) | agent_l2, mlro |
cases:bulk | cases | bulk operations | mlro |
policies:read | policies | read | agent_l2, mlro, compliance_officer, auditor, dsi_admin |
policies:edit | policies | edit draft | mlro, compliance_officer |
policies:publish | policies | publish (dual-control) | mlro, compliance_officer, dsi_admin |
forms:read | forms | read | tous |
forms:edit | forms | edit draft | dsi_admin, compliance_officer |
forms:publish | forms | publish | dsi_admin, compliance_officer |
sanctions:screen | sanctions | trigger screening | agent_l1+ |
sanctions:admin | sanctions | admin lists + thresholds | mlro, compliance_officer |
audit:read | audit | read trail | tous |
audit:replay | audit | replay screening | mlro, auditor |
audit:export | audit | export PDF (step-up) | mlro, auditor, dsi_admin |
users:read | users | list | dsi_admin, auditor |
users:manage | users | CRUD users | dsi_admin |
webhooks:read | webhooks | read config | dsi_admin, developer |
webhooks:manage | webhooks | CRUD config | dsi_admin |
tenant:read | tenant | read config | tous |
tenant:manage | tenant | edit tenant config | dsi_admin |
4.2 ABAC — attributs additionnels
Section intitulée « 4.2 ABAC — attributs additionnels »Au-delà du RBAC standard, on enforce :
- Tenant isolation :
principal.tenantId == resource.tenantId(requis sur 100 % des endpoints — RLS Postgres en backstop) - Seniority :
principal.seniority.atLeast(case.minSeniority)pour escalades - Skills match : router cases requiert
case.requiredSkills ⊆ principal.skills - Assurance :
principal.mfa_acr >= action.minAcrpour actions sensibles - Step-up window :
now < principal.step_up_untilpour publish/confirm
4.3 Implémentation pure Kotlin (shared/auth-client-jvm)
Section intitulée « 4.3 Implémentation pure Kotlin (shared/auth-client-jvm) »// Pure function enforcement — testable, deterministic, no side effectsobject Authz { fun require(principal: VitaKycPrincipal, permission: Permission) fun requireSeniority(principal: VitaKycPrincipal, min: Seniority) fun requireAssurance(principal: VitaKycPrincipal, min: AssuranceLevel) fun requireStepUp(principal: VitaKycPrincipal, now: Instant = Clock.System.now()) fun requireSameTenant(principal: VitaKycPrincipal, resourceTenantId: String)}
// Ktor middleware example (in each service)authenticate("vitakyc-jwt") { post("/cases/{id}/decide") { val principal = call.vitaKycPrincipal() Authz.require(principal, Permission.CASES_DECIDE) Authz.requireSameTenant(principal, requestedCase.tenantId) // ... actual logic }
post("/cases/{id}/confirm") { val principal = call.vitaKycPrincipal() Authz.require(principal, Permission.CASES_CONFIRM) Authz.requireStepUp(principal) // 4-eyes seconde sig requires recent MFA // ... }}Tests unitaires : 14 tests passants couvrant hasPermission, requireAll, requireAny, requireSeniority, requireAssurance, isStepUpActive, requireStepUp, requireSameTenant. Cf AuthzTest.kt.
5. MFA — TOTP + WebAuthn enrollment
Section intitulée « 5. MFA — TOTP + WebAuthn enrollment »5.1 First login flow (forced enrollment)
Section intitulée « 5.1 First login flow (forced enrollment) »5.2 Login récurrent
Section intitulée « 5.2 Login récurrent »Password + TOTP (6 digits temps-réel) ou password + WebAuthn (single tap YubiKey).
5.3 Recovery — TOTP secret perdu
Section intitulée « 5.3 Recovery — TOTP secret perdu »- Agent contacte DSI tenant via canal hors-bande (téléphone, email).
- DSI valide identité (questions vérification, doc ID).
- DSI back-office VitaKYC
Reset MFA(action audited + step-up MFA DSI requis). - Agent re-login → forced enrollment new TOTP/WebAuthn.
5.4 Politique MFA enforcée realm
Section intitulée « 5.4 Politique MFA enforcée realm »Keycloak realm vitakyc-saas browser flow :
1. Cookie (SSO si valide)2. ALTERNATIVE ├── User Identifier └── Password Form ALTERNATIVE ├── Password Form └── WebAuthn Authenticator3. CONDITIONAL └── If MFA NOT enrolled → REQUIRED enroll TOTP/WebAuthn4. REQUIRED OTP/WebAuthn challenge6. Step-up MFA
Section intitulée « 6. Step-up MFA »6.1 Use case
Section intitulée « 6.1 Use case »Actions sensibles exigent que l’utilisateur ait re-prouvé son identité dans les 15 dernières minutes — même si la session SSO est encore active 7h.
6.2 Flow
Section intitulée « 6.2 Flow »6.3 7 actions step-up
Section intitulée « 6.3 7 actions step-up »| Permission | Pourquoi |
|---|---|
policies:publish | dual-control + audit BCT |
cases:confirm | 4-eyes (cf ADR-029) |
audit:export | export PDF audit signé |
audit:replay | replay screening (sensitive read) |
secrets:rotate | webhook secret, API key tenant |
users:manage | création/suppression users (privileged) |
forms:publish | dual-control + impact CPS (cf ADR-026) |
7. Federation tenant tier-1
Section intitulée « 7. Federation tenant tier-1 »7.1 OIDC federation (Azure AD, Okta, autre IdP)
Section intitulée « 7.1 OIDC federation (Azure AD, Okta, autre IdP) »Realm dédié tn-banquex :
{ "alias": "azure-ad-banquex", "provider_id": "oidc", "config": { "authorization_url": "https://login.microsoftonline.com/<tenant-azure>/oauth2/v2.0/authorize", "token_url": "https://login.microsoftonline.com/<tenant-azure>/oauth2/v2.0/token", "client_id": "<azure-app-id>", "client_secret": "<vault://tenant/TN-BANQUEX/azure-client-secret>", "default_scope": "openid profile email", "user_name_attribute": "email", "sync_mode": "FORCE", "trust_email": "true" }}Mapping claims Azure → Keycloak roles via mappers (groupes Azure → roles VitaKYC).
7.2 SAML 2.0 federation (banque legacy)
Section intitulée « 7.2 SAML 2.0 federation (banque legacy) »Idem mais providerId saml. SP metadata exposé https://auth.vitakyc.io/realms/tn-banquex/broker/saml-banquex/endpoint/descriptor.
7.3 Provisioning
Section intitulée « 7.3 Provisioning »- V0 MVP : import CSV agents (
firstName, lastName, email, role, seniority, skills) → admin DSI VitaKYC. Email d’invitation magic-link. Forced MFA enrollment au premier login. - V1 : SCIM 2.0 endpoint exposé
https://auth.vitakyc.io/realms/tn-banquex/scim/v2/Users, configurable côté Azure AD / Okta.
8. Audit trail signé
Section intitulée « 8. Audit trail signé »8.1 Events Keycloak → Kafka → audit-svc
Section intitulée « 8.1 Events Keycloak → Kafka → audit-svc »Chaque event Keycloak emis sur Kafka auth.audit.v1 :
{ "event_id": "evt_8a7f3c1e9d4b2a6f", "type": "LOGIN", "realm_id": "tn-banquex", "user_id": "u_xxx", "tenant_id": "TN-BANQUEX", "session_id": "sess_xxx", "ip_address": "41.226.xxx.xxx", "user_agent": "Mozilla/5.0 ...", "details": { "auth_method": "openid-connect", "amr": ["pwd","totp"] }, "occurred_at": "2026-04-28T11:42:00Z"}Types tracés : LOGIN, LOGIN_ERROR, LOGOUT, REGISTER, IDENTITY_PROVIDER_LOGIN, MFA_ENROLLED, MFA_USED, STEP_UP_REQUESTED, STEP_UP_COMPLETED, ROLE_ASSIGNED, ROLE_REVOKED, PASSWORD_CHANGED, MFA_RESET, BRUTE_FORCE_DETECTED.
8.2 Signature + chainage côté audit-svc
Section intitulée « 8.2 Signature + chainage côté audit-svc »Identique pattern ADR-029 / ADR-030 / ADR-032 : SHA-256 chainage previousEventHash + Ed25519 signature par tenant key.
8.3 Schéma Postgres
Section intitulée « 8.3 Schéma Postgres »CREATE TABLE auth_audit_event ( event_id UUID PRIMARY KEY, realm_id VARCHAR(64) NOT NULL, tenant_id UUID NOT NULL, user_id VARCHAR(64), session_id VARCHAR(64), type VARCHAR(32) NOT NULL, ip_address INET, user_agent TEXT, details JSONB, occurred_at TIMESTAMPTZ NOT NULL, previous_event_hash CHAR(74) NOT NULL, hash CHAR(74) NOT NULL, signature TEXT NOT NULL);
ALTER TABLE auth_audit_event ENABLE ROW LEVEL SECURITY;CREATE POLICY auth_audit_tenant_isolation ON auth_audit_event USING (tenant_id = current_setting('app.current_tenant_id')::uuid);REVOKE UPDATE, DELETE ON auth_audit_event FROM app_role;Rétention 10 ans WORM (cf ADR-002).
9. API REST auth-svc
Section intitulée « 9. API REST auth-svc »9.1 Endpoints user-facing
Section intitulée « 9.1 Endpoints user-facing »| Méthode | Endpoint | Description | Auth |
|---|---|---|---|
GET | /v1/me | principal info (parsed from JWT) | bearer |
GET | /v1/sessions | list user’s active sessions | bearer |
DELETE | /v1/sessions/:id | revoke a session | bearer + step-up |
POST | /v1/step-up/init | initiate step-up MFA challenge | bearer |
POST | /v1/step-up/verify | verify TOTP/WebAuthn + issue new JWT with step_up_until | bearer |
POST | /v1/mfa/enroll/totp | enroll TOTP (returns QR code + secret) | bearer |
POST | /v1/mfa/enroll/webauthn | enroll WebAuthn passkey (challenge) | bearer |
DELETE | /v1/mfa/:authId | remove MFA factor | bearer + step-up |
9.2 Endpoints admin DSI
Section intitulée « 9.2 Endpoints admin DSI »| Méthode | Endpoint | Description | Auth |
|---|---|---|---|
GET | /v1/users | list users tenant | bearer + users:read |
POST | /v1/users | create user (sends invitation magic-link) | bearer + users:manage + step-up |
PUT | /v1/users/:id | update roles/skills/seniority | bearer + users:manage + step-up |
DELETE | /v1/users/:id | disable user | bearer + users:manage + step-up |
POST | /v1/users/:id/reset-mfa | force MFA reset | bearer + users:manage + step-up |
POST | /v1/users/:id/unlock | unlock after brute force lockout | bearer + users:manage |
GET | /v1/users/:id/audit | user-specific audit trail | bearer + audit:read |
9.3 Endpoints inter-services
Section intitulée « 9.3 Endpoints inter-services »| Méthode | Endpoint | Description | Auth |
|---|---|---|---|
GET | /internal/jwks | proxy Keycloak JWKS (cache 1h) | mTLS |
POST | /internal/validate | validate JWT + return parsed principal | mTLS |
GET | /internal/health | liveness | none |
GET | /internal/ready | readiness (Keycloak reachable + DB ok) | none |
9.4 OpenAPI
Section intitulée « 9.4 OpenAPI »Spec exposée https://auth.vitakyc.io/openapi/auth-svc.yaml. Contract tests Pact en CI.
10. Frontend integration
Section intitulée « 10. Frontend integration »10.1 React back-office
Section intitulée « 10.1 React back-office »pnpm add react-oidc-context oidc-client-tsimport { AuthProvider } from "react-oidc-context";
const oidcConfig = { authority: "https://auth.vitakyc.io/realms/vitakyc-saas", client_id: "vitakyc-back-office", redirect_uri: window.location.origin + "/auth/callback", post_logout_redirect_uri: window.location.origin, response_type: "code", scope: "openid profile email", loadUserInfo: true, automaticSilentRenew: true, monitorSession: true, filterProtocolClaims: true,};
<AuthProvider {...oidcConfig}> <App /></AuthProvider>;// App.tsx — protected routeimport { useAuth } from "react-oidc-context";
function ProtectedRoute({ children, requirePermission }: ...) { const auth = useAuth(); if (!auth.isAuthenticated) return <Login />; const principal = parsePrincipalFromJwt(auth.user.access_token); if (requirePermission && !principal.permissions.includes(requirePermission)) return <Forbidden />; return <>{children}</>;}10.2 Step-up modal
Section intitulée « 10.2 Step-up modal »// On 401 step_up_required from API:function handleStepUpRequired(returnTo: string) { await auth.signinPopup({ prompt: "login", acr_values: "urn:mace:incommon:iap:silver" }); // refreshed token now has step_up_until set retry(returnTo);}10.3 SDK Web (form runtime client KYC)
Section intitulée « 10.3 SDK Web (form runtime client KYC) »Le SDK Web côté client final n’utilise pas Keycloak (le client n’est pas un user authentifié). Il utilise une API key tenant + session token éphémère :
const session = await vitakyc.startSession({ apiKey: "pk_live_TN-BANQUEX_xxx", formId: "FORM_KYC_INDIVIDUAL", externalRef: "CASE-42"});// session.token short-lived (TTL 30 min) bound to (tenantId, formId, externalRef)11. Dockerfile auth-svc
Section intitulée « 11. Dockerfile auth-svc »# Stage 1 — build with JDK 21FROM gradle:8.10.2-jdk21 AS buildWORKDIR /home/gradle/srcCOPY settings.gradle.kts build.gradle.kts gradlew gradlew.bat ./COPY gradle gradleCOPY platform/auth-svc platform/auth-svcCOPY shared/auth-client-jvm shared/auth-client-jvmRUN ./gradlew :platform:auth-svc:buildFatJar --no-daemon
# Stage 2 — runtime Eclipse Temurin JRE 21FROM eclipse-temurin:21-jre-alpineRUN addgroup -S app && adduser -S app -G appWORKDIR /appCOPY --from=build --chown=app:app /home/gradle/src/platform/auth-svc/build/libs/auth-svc.jar app.jarUSER appEXPOSE 8080HEALTHCHECK --interval=10s --timeout=3s --start-period=20s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75", "-jar", "app.jar"]Image résultat ~250 MB (JRE 21 alpine + 30 MB fat jar).
12. Performance et capacité
Section intitulée « 12. Performance et capacité »| Metric | MVP cible | V2 cible |
|---|---|---|
| Latence p95 login (incl MFA) | ≤ 3 s | ≤ 1.5 s |
| Latence p95 JWT validation côté services | ≤ 5 ms | ≤ 2 ms |
| Throughput auth-svc | ≥ 100 req/s | ≥ 500 req/s |
| JWKS cache hit rate | ≥ 99,9 % | ≥ 99,99 % |
| Disponibilité Keycloak | 99,5 % | 99,9 % |
| Token refresh latency p95 | ≤ 200 ms | ≤ 100 ms |
13. Sécurité
Section intitulée « 13. Sécurité »- HTTPS partout TLS 1.2+ (TLS 1.0/1.1 refusés)
- Cookies
Secure; HttpOnly; SameSite=Strict - Tokens : stockés en memory + httpOnly cookie pour refresh, jamais localStorage (XSS)
- Anti-CSRF : tokens sur tous endpoints mutating
- CSP strict :
default-src 'self'; script-src 'self' 'unsafe-inline' (dev only) - HSTS :
max-age=31536000; includeSubDomains; preload - JWT signing : RS256 (asymétrique, services valident avec public key JWKS)
- Brute force : Keycloak Admin Brute Force Detection — 10 fail / 5 min → 15 min lockout
- Rate limit auth-svc : 60 login/min/IP, 10 step-up/min/user
- PII : email + nom dans JWT seulement (jamais password, jamais TOTP secret)
- Audit : signed Ed25519 chainée 10 ans WORM
- Vault : tenant signing keys + IdP federation client secrets stockés HSM mode
14. Plan de migration MVP → V2
Section intitulée « 14. Plan de migration MVP → V2 »| Item | MVP (V0) | V2 (S+12) |
|---|---|---|
| Realms | hybride saas + tier-1 | + multi-region (TN + UAE + EU) |
| MFA | TOTP + WebAuthn | + push mobile FreeOTP+ |
| Federation | OIDC + SAML | + SCIM 2.0 provisioning |
| Smart Card | non | optionnel pour ANSI Tunisie tier-1 (V1) |
| OAuth scopes fine-grained | permissions enum | + dynamic scopes par module |
| Anomaly detection | Keycloak brute force basique | + UEBA (User Entity Behavior Analytics) |
| Passkeys cross-device | Keycloak 25 minimum | sync passkeys iCloud/Google |
15. Checklist go-live MVP
Section intitulée « 15. Checklist go-live MVP »- Keycloak 25.x déployé HA (3 replicas + Postgres dédié)
- Realm
vitakyc-saasconfiguré + 7 roles + 16 permissions + browser flow MFA - auth-svc Ktor déployé, JWT validation lib testée
- shared/auth-client-jvm publié + 14 tests verts
- Themes brandés VitaKYC (login, MFA, error pages) FR/AR/EN
- Audit pipeline Keycloak → Kafka → audit-svc signed Ed25519 chainée
- Brute force detection + rate limit configurés
- Recovery procedure documentée DSI
- WebAuthn enrollment forcé DSI + MLRO
- Step-up MFA enforcé sur 7 actions sensibles
- Frontend back-office OIDC code+PKCE intégré + step-up modal
- Federation OIDC tenant pilote tier-1 testée
- DPIA mise à jour (cf DPIA)
- Pilote tenant : 5 agents L1 + 2 L2 + 1 MLRO + 1 DSI MFA enrolled, 0 incident sécurité
16. Références
Section intitulée « 16. Références »- ADR-002, ADR-009, ADR-029, ADR-032, ADR-033
- Standards : OIDC Core 1.0, PKCE RFC 7636, WebAuthn Level 2, NIST SP 800-63B
- Compliance : BCT Circulaire 2017-08 + 2018-07, RGPD art. 32, ANSI référentiel, ISO 27001 A.9
- Repo platform : vitakyc-platform
- Lib partagée :
shared/auth-client-jvm(14 tests verts) - Service :
platform/auth-svc(skeleton + 2 tests verts) - Realm template :
infra/keycloak/realm-template/ - Convention industrie : Stripe SCA + step-up, Auth0 step-up best practices
Document de spec auth system — version 1.0 (2026-04-28).