PD-26 — Configuration OAuth2 / OIDC avec Keycloak¶
📚 Navigation User Story
| Document | | | ---------- | -- | | 📋 **Spécification** | *(ce document)* | | 🛠️ [Plan d'implémentation](PD-26-plan.md) | | | 🧪 [Tests contractuels](PD-26-tests.md) | | | ✅ Critères d'acceptation | *(à venir)* | | 📝 Retour d'expérience | *(à venir)* | [← Retour à auth-identity](../PD-182-epic.md) · [↑ Index User Story](index.md)1. Objectif¶
Définir une spécification canonique contractuelle pour la mise en place et l’usage d’un fournisseur d’identité Keycloak afin d’assurer une authentification et autorisation OAuth2 / OpenID Connect robustes, déterministes, auditables et exploitables en production pour un backend NestJS.
La spécification fait loi. Toute implémentation doit démontrer sa conformité par des critères et scénarios testables.
2. Périmètre / Hors périmètre¶
Inclus¶
- Déploiement et configuration d’un Keycloak opérationnel pour au minimum deux environnements (non‑prod et prod).
- Création d’un realm par environnement (ou mécanisme d’isolation équivalent).
- Déclaration et configuration des clients OIDC nécessaires (backend et, le cas échéant, front/mobile).
- Configuration des mappers pour exposer les claims requis dans les tokens.
- Validation JWT côté backend (issuer, audience, signature, algorithme, dates).
- Application des règles d’autorisation basées sur rôles et/ou scopes.
- Journalisation et observabilité minimales permettant l’audit de sécurité.
Exclu¶
- Fédération SSO avec des IdP externes (Google, Microsoft, FranceConnect, SAML).
- MFA (TOTP, WebAuthn, SMS) sauf exigence explicitement ajoutée.
- Gestion complète du cycle de vie des identités (KYC, identité qualifiée eIDAS).
- Politiques RGPD détaillées (durées, droits) hors exigences explicitement fournies.
- Gestion des secrets applicatifs et rotation infra hors points explicitement listés.
3. Définitions¶
- OAuth2 : Framework d’autorisation basé sur des access tokens.
- OIDC : Couche d’identité au‑dessus d’OAuth2 (id_token, discovery).
- Keycloak : Fournisseur d’identité supportant OAuth2/OIDC.
- Realm : Espace de sécurité isolé (utilisateurs, clients, rôles).
- Client : Application enregistrée auprès du realm.
- Mapper : Règle d’exposition/transformation de claims.
- JWT : Jeton signé contenant des claims.
- Issuer (iss) : Autorité émettrice du token.
- Audience (aud) : Destinataire(s) du token.
- JWKS : Jeu de clés publiques pour la vérification des signatures.
- Role / Scope : Mécanismes d’autorisation.
4. Invariants (non négociables)¶
| ID | Règle | Justification |
|---|---|---|
| INV‑01 | Aucune hypothèse implicite : toute valeur de config (issuer, aud, algos, durées, flux) est explicitée. | Testabilité contractuelle |
| INV‑02 | Le backend vérifie signature, iss, aud, exp/nbf et alg autorisé. | Sécurité JWT |
| INV‑03 | Aucun token dont la vérification échoue n’est accepté. | Sécurité |
| INV‑04 | Aucun token ou secret n’est loggé en clair. | Confidentialité |
| INV‑05 | Chaque requête authentifiée est attribuable à une identité et un contexte d’autorisation. | Auditabilité |
| INV‑06 | Les règles d’authN/authZ sont déterministes et testables. | Robustesse |
| INV‑07 | Les environnements sont isolés (au minimum par realm). | Cloisonnement |
| INV‑08 | En cas d’indisponibilité JWKS/Discovery, le système est fail‑closed. | Résilience |
| INV‑09 | Tout token présenté contient l’ensemble des claims normatifs requis (sub, iss, aud, exp, iat, tenant, authz) ; absence/nullité/non‑conformité d’un seul claim → refus systématique. | Testabilité / audit |
| INV‑10 | Le modèle d’autorisation est normatif et unique : AUTHORIZATION_MODEL = HYBRID avec authz.roles (rôles applicatifs) et authz.scopes (scopes OAuth2). Absence simultanée de roles et scopes → refus. | Robustesse |
| INV‑11 | La décision d'autorisation est déterministe : exigences de route (rôle/scope, AND/OR déclaré) doivent correspondre explicitement au contenu du token, sinon refus systématique ; aucune règle implicite ni héritage non déclaré. | Sécurité / conformité |
| INV‑12 | Protection brute force : l'IdP verrouille temporairement un compte après N tentatives d'authentification infructueuses consécutives (bruteForceProtected: true, failureFactor: N). Délégué à Keycloak, non observable côté backend. | Sécurité / anti-abus |
Toutes les règles relatives à la validation des tokens d’identité et d’autorisation (issuer, audiences, claims requis, contraintes temporelles, algorithmes cryptographiques, politique de flux et isolation par environnement) sont définies de manière transverse dans la User Story PD‑235 et s’appliquent intégralement à la présente spécification.
4.1 Cas PD‑235 délégués au backend (traçabilité)¶
- Les cas PD‑235 dépendants du consommateur backend sont pris en charge ici :
- PD‑235 TC‑ERR‑01 / TC‑ERR‑03 : IdP/JWKS/Discovery indisponible → fail‑closed et signal explicite (health/readiness/metric) côté backend.
- PD‑235 TC‑NEG‑02 : token expiré rejeté (validation
exp/iat+ skew) côté backend. - PD‑235 TC‑NEG‑04 : token sans identité valide (sub/tenant/claims requis absents) rejeté.
- Les scénarios associés sont définis dans PD‑26-tests.md (section 2bis) avec des alias
TC-PD235-*afin d’assurer la traçabilité et la clôture de la dépendance PD‑235.
5. Flux nominaux¶
N1 — Découverte OIDC¶
- Récupération du document de découverte.
- Validation de l’issuer et récupération du JWKS.
N2 — Authentification utilisateur¶
- L’utilisateur s’authentifie via le flux OIDC défini (à préciser).
- Le client obtient au minimum un access_token.
N3 — Appel API protégé¶
- Le client appelle une API avec un access_token.
- Le backend valide le token selon les invariants.
- Extraction des claims nécessaires.
- Autorisation ou refus.
N4 — Autorisation par rôles/scopes¶
- Une route déclare des exigences.
- Le backend compare avec les claims.
- Accès accordé ou refusé.
N5 — Rotation des clés (JWKS)¶
- Publication d’un nouveau JWKS.
- Adoption selon la politique définie.
- Rejet des tokens signés par une clé retirée.
5bis. Diagrammes¶
5bis.1 Diagramme d’états — Validation JWT (INV-02, INV-03, INV-09)¶
stateDiagram-v2
[*] --> TokenReceived : Requête entrante
TokenReceived --> CheckPresence : Extraire Authorization header
CheckPresence --> Rejected_E1 : Token absent [INV-03]
CheckPresence --> CheckFormat : Token présent
CheckFormat --> Rejected_E2 : Malformé [INV-03]
CheckFormat --> CheckAlgorithm : JWT parsé
CheckAlgorithm --> Rejected_E7 : alg ∉ JWT_ALLOWED_ALGORITHMS [INV-02]
CheckAlgorithm --> CheckSignature : alg autorisé (RS256)
CheckSignature --> Rejected_E3 : Signature invalide / kid inconnu [INV-02]
CheckSignature --> CheckIssuer : Signature valide
CheckIssuer --> Rejected_E4 : iss ≠ OIDC_ISSUER [INV-02]
CheckIssuer --> CheckAudience : iss valide
CheckAudience --> Rejected_E5 : aud ∉ OIDC_ALLOWED_AUDIENCES [INV-02]
CheckAudience --> CheckExpiry : aud valide
CheckExpiry --> Rejected_E6 : exp/nbf hors tolérance (skew 120s) [INV-02]
CheckExpiry --> CheckClaims : Dates valides
CheckClaims --> Rejected_E8 : Claims requis manquants (sub/iss/aud/exp/iat/tenant/authz) [INV-09]
CheckClaims --> CheckAuthz : Claims complets
CheckAuthz --> Rejected_E9 : Droits insuffisants (HYBRID: roles ∩ scopes) [INV-10, INV-11]
CheckAuthz --> Accepted : Autorisé
Accepted --> [*]
state Rejected_E1 : 401 — Token absent
state Rejected_E2 : 401 — Token malformé
state Rejected_E3 : 401 — Signature invalide
state Rejected_E4 : 401 — Issuer invalide
state Rejected_E5 : 401 — Audience invalide
state Rejected_E6 : 401 — Token expiré
state Rejected_E7 : 401 — Algorithme interdit
state Rejected_E8 : 401 — Claims manquants
state Rejected_E9 : 403 — Droits insuffisants
state Accepted : 200 — Requête traitée 5bis.2 Diagramme de séquence — Authentification OIDC et appel API (N1–N4)¶
sequenceDiagram
participant C as Client (frontend/mobile)
participant KC as Keycloak (IdP)
participant B as Backend NestJS
note over C, KC: N1 — Découverte OIDC
B->>KC: GET /.well-known/openid-configuration
KC-->>B: Discovery document (issuer, jwks_uri, ...)
B->>KC: GET /protocol/openid-connect/certs
KC-->>B: JWKS (clés publiques RS256)
note over C, KC: N2 — Authentification utilisateur
C->>KC: Authorization Code Flow (+PKCE si mobile)
KC-->>C: code d’autorisation
C->>KC: POST /token (code + client_id)
KC-->>C: access_token + id_token (JWT signé RS256)
note over C, B: N3 — Appel API protégé [INV-02, INV-03]
C->>B: GET /api/resource (Authorization: Bearer <access_token>)
rect rgb(240, 248, 255)
note right of B: Pipeline de validation JWT
B->>B: Vérifier alg ∈ {RS256} [INV-02]
B->>B: Vérifier signature via JWKS [INV-02]
B->>B: Vérifier iss = OIDC_ISSUER [INV-02]
B->>B: Vérifier aud ∈ {pv-backend, pv-api} [INV-02]
B->>B: Vérifier exp/nbf ± 120s [INV-02]
B->>B: Vérifier claims requis (sub, tenant, authz) [INV-09]
end
note over C, B: N4 — Autorisation HYBRID [INV-10, INV-11]
rect rgb(240, 255, 240)
B->>B: Extraire authz.roles + authz.scopes
B->>B: Comparer avec exigences de route (AND/OR)
B->>B: Émettre entrée d’audit [INV-04, INV-05]
end
alt Token valide + droits suffisants
B-->>C: 200 OK + payload
else Token invalide [INV-03]
B-->>C: 401 Unauthorized (aucun détail token loggé [INV-04])
else Droits insuffisants [INV-11]
B-->>C: 403 Forbidden
end 5bis.3 Diagramme d’états — Service OIDC / JWKS (INV-08)¶
stateDiagram-v2
[*] --> Initializing : Démarrage service
Initializing --> Healthy : Discovery + JWKS chargés
Initializing --> Degraded : Discovery ou JWKS indisponible [INV-08]
Healthy --> Healthy : Rotation JWKS réussie (N5)
Healthy --> Degraded : JWKS/Discovery indisponible
Degraded --> Healthy : JWKS/Discovery restauré
Degraded --> Degraded : Tentative échouée
state Healthy {
[*] --> AcceptingTokens
AcceptingTokens : Validation JWT active
AcceptingTokens : Health/readiness = OK
}
state Degraded {
[*] --> FailClosed
FailClosed : Tout nouveau token rejeté [INV-08]
FailClosed : Health/readiness = DEGRADED
FailClosed : Metric OIDC indisponible exposée (CA-11)
} 5bis.4 Diagramme de séquence — Rotation JWKS (N5)¶
sequenceDiagram
participant KC as Keycloak (IdP)
participant B as Backend NestJS
participant C as Client
note over KC, B: N5 — Rotation des clés JWKS [INV-02, INV-08]
KC->>KC: Générer nouvelle paire de clés (kid=new)
KC->>KC: Publier JWKS avec {kid=old, kid=new}
B->>KC: GET /protocol/openid-connect/certs (refresh périodique)
KC-->>B: JWKS mis à jour (old + new)
B->>B: Mettre à jour le keystore local
note over C, B: Tokens signés avec ancienne clé (kid=old)
C->>B: GET /api/resource (token signé kid=old)
B->>B: kid=old encore dans JWKS → signature valide ✓
B-->>C: 200 OK
KC->>KC: Retirer kid=old du JWKS (après période de grâce)
B->>KC: GET /protocol/openid-connect/certs (refresh)
KC-->>B: JWKS avec {kid=new} uniquement
note over C, B: Token signé avec clé retirée (kid=old)
C->>B: GET /api/resource (token signé kid=old)
B->>B: kid=old absent du JWKS → rejet [INV-02, INV-03]
B-->>C: 401 Unauthorized 6. Cas d’erreur¶
- E1 Token absent → refus non authentifié.
- E2 Token malformé → refus non authentifié.
- E3 Signature invalide/JWKS inconnu → refus.
- E4 Issuer inattendu → refus.
- E5 Audience invalide → refus.
- E6 Token expiré/non valide → refus.
- E7 Algorithme interdit → refus.
- E8 Claims requis manquants → refus.
- E9 Droits insuffisants → refus.
- E10 Mauvaise configuration → service non prêt.
7. Critères d’acceptation (testables)¶
| ID | Critère | Observable |
|---|---|---|
| CA‑01 | Requête sans token rejetée | Code/état |
| CA‑02 | Signature invalide rejetée | Code/état |
| CA‑03 | Issuer incorrect rejeté | Code/état |
| CA‑04 | Audience incorrecte rejetée | Code/état |
| CA‑05 | Token expiré rejeté | Code/état |
| CA‑06 | Algo non autorisé rejeté | Code/état |
| CA‑07 | Claims requis présents (sub, iss, aud, exp, iat, tenant, authz) | Refus si manquant |
| CA‑08 | Autorisation déterministe (HYBRID : authz.roles / authz.scopes, règles AND/OR déclarées) | Accès/refus traçable |
| CA‑09 | Isolation env respectée (realm/tenant explicite) | Refus si cross-realm/tenant |
| CA‑10 | Aucune fuite dans les logs (liste normative) | Logs conformes à la liste blanche |
| CA‑11 | Fail‑closed JWKS/Discovery avec signal d’état dégradé | État service (code/flag/metric) |
8. Scénarios de test (GWT)¶
Les scénarios de référence sont décrits dans PD‑26‑tests.md et couvrent l’ensemble des critères ci‑dessus.
9. Hypothèses explicites¶
| ID | Hypothèse | Impact si faux |
|---|---|---|
| H‑01 | Keycloak est l’IdP retenu | Refonte spec |
| H‑02 | Backend NestJS | Adaptation technique |
| H‑03 | OAuth2/OIDC utilisé | Refonte |
10. Points à clarifier¶
- Typologie des clients (public/confidential).
- Détermination du tenant et règles multi‑tenant (valeurs métier).
- Durées de vie des tokens et sessions.
- Politique de révocation et de rotation.
- Exigences TLS/mTLS inter‑services.
- Liste concrète des rôles/scopes applicatifs et mapping interne IdP (valeurs métier).
11. Paramètres contractuels OIDC / JWT (obligatoires)¶
- OIDC_ISSUER : valeur attendue du claim
iss. Tout token dontiss≠OIDC_ISSUERest rejeté. - OIDC_ALLOWED_AUDIENCES (liste fermée) : le claim
auddoit contenir au moins une valeur de cette liste. Absence ou incompatibilité → rejet. - JWT_ALLOWED_ALGORITHMS (liste fermée) et JWT_FORBIDDEN_ALGORITHMS (incluant au minimum
none) : tout token dontalg∉JWT_ALLOWED_ALGORITHMSest rejeté ; tout algorithme non autorisé est implicitement interdit. - JWT_REQUIRED_CLAIMS (liste fermée) : {
sub,iss,aud,exp,iat,<tenant>,<authz>}. L’absence d’un seul claim requis entraîne un rejet. Le nom du claim tenant et du claim d’autorisation est contractuel. - AUTHORIZATION_MODEL ∈ {
RBAC,SCOPES,HYBRID} : - Si
RBAC: claim de rôles =JWT_ROLE_CLAIM(nom contractuel). - Si
SCOPES: claim de scopes =JWT_SCOPE_CLAIM(nom contractuel). - Si
HYBRID: combiner les deux règles ci-dessus. Tout token ne satisfaisant pas le modèle déclaré est rejeté. - JWT_CLOCK_SKEW_MAX : tolérance maximale (durée) appliquée à
exp/nbf/iat. Toute dérive au-delà de cette tolérance entraîne un rejet. - ALLOWED_OIDC_FLOWS (par type de client) : pour chaque
CLIENT_TYPE∈ {backend,frontend,mobile,service}, la liste des flux OIDC autorisés est définie ; tout token issu d’un flux non autorisé pour le type déclaré est rejeté.
12. Règles d’isolation realm / tenant (normatives)¶
- REALM_POLICY : chaque environnement (dev, staging, prod, etc.) dispose d’un realm distinct et non partagé.
- TENANT_POLICY : chaque tenant est associé à un realm unique OU à un identifiant de tenant contractuel (
tenant) dans les claims JWT ; le mapping realm↔tenant est explicite. - Toute requête contenant un token dont le realm/tenant ne correspond pas à l’environnement cible est rejetée (cross-realm/tenant interdit).
- Les valeurs concrètes des realms et tenants sont publiées contractuellement (hors scope de ce document), mais la règle de refus cross-realm/tenant est normative.
13. Paramètres explicitement hors périmètre (valeurs non normées mais existence requise)¶
- Valeurs numériques exactes des durées (ex : 3600s) et URLs physiques des endpoints.
- Mécanismes de cache JWKS.
- Choix Keycloak spécifiques (mappers internes, noms techniques) — seuls les noms contractuels des claims requis sont normatifs.
- L’existence et la publication de ces paramètres sont obligatoires, mais leurs valeurs opérationnelles ne sont pas fixées par le présent contrat.
14. Politique de log/audit (normative)¶
- Champs interdits : tokens (access/refresh/id), en-tête Authorization complet, secrets/signatures, clés privées, claims sensibles non listés, payload JWT en clair.
- Champs autorisés (liste blanche fermée) : requestId, sub (ou identifiant utilisateur contractuel), tenant, issuer, audience, clientId, code d’erreur/refus, route ou ressource, horodatage. Tout champ non listé est interdit.
- Granularité minimale : chaque décision d’accès (succès ou refus) produit une entrée d’audit conforme à la liste blanche, sans données sensibles, avec un niveau de détail constant (pas de debug conditionnel).
15. État dégradé JWKS/Discovery (fail-closed observabilité)¶
- En cas d’indisponibilité de JWKS ou du document de découverte, le service rejette tout nouveau token (fail-closed) ET expose un état dégradé observable.
- Signal requis (au moins un) : code/flag dans l’endpoint health/readiness ou metric explicite indiquant l’indisponibilité du fournisseur OIDC.
- Ce signal est normatif pour la vérification du critère CA‑11 et du test TC-NOM-11.
16. Annexe — Paramètres contractuels (valeurs normatives)¶
- OIDC_ISSUER =
https://auth.probatiovault.prod/realms/pv-prod - OIDC_ALLOWED_AUDIENCES = {
pv-backend,pv-api} - JWT_ALLOWED_ALGORITHMS = {
RS256} - JWT_FORBIDDEN_ALGORITHMS = {
none} - JWT_REQUIRED_CLAIMS = {
sub,iss,aud,exp,iat,tenant,authz} - AUTHORIZATION_MODEL =
HYBRID - JWT_ROLE_CLAIM =
authz.roles - JWT_SCOPE_CLAIM =
authz.scopes - JWT_CLOCK_SKEW_MAX = 120s
- CLIENT_TYPE / ALLOWED_OIDC_FLOWS :
- backend : {
client_credentials} - frontend : {
authorization_code} - mobile : {
authorization_code,pkce} - service : {
client_credentials}
Références¶
- Epic : PD‑182 — AUTH
- JIRA : PD‑26
- Repos : backend / infra