Aller au contenu

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

  1. Récupération du document de découverte.
  2. Validation de l’issuer et récupération du JWKS.

N2 — Authentification utilisateur

  1. L’utilisateur s’authentifie via le flux OIDC défini (à préciser).
  2. Le client obtient au minimum un access_token.

N3 — Appel API protégé

  1. Le client appelle une API avec un access_token.
  2. Le backend valide le token selon les invariants.
  3. Extraction des claims nécessaires.
  4. Autorisation ou refus.

N4 — Autorisation par rôles/scopes

  1. Une route déclare des exigences.
  2. Le backend compare avec les claims.
  3. Accès accordé ou refusé.

N5 — Rotation des clés (JWKS)

  1. Publication d’un nouveau JWKS.
  2. Adoption selon la politique définie.
  3. 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

  1. Typologie des clients (public/confidential).
  2. Détermination du tenant et règles multi‑tenant (valeurs métier).
  3. Durées de vie des tokens et sessions.
  4. Politique de révocation et de rotation.
  5. Exigences TLS/mTLS inter‑services.
  6. 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 dont issOIDC_ISSUER est rejeté.
  • OIDC_ALLOWED_AUDIENCES (liste fermée) : le claim aud doit 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 dont algJWT_ALLOWED_ALGORITHMS est 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