Aller au contenu

dynors-auth — Service d'authentification centralisé

État d'implémentation (2026-06)

MVP codé (dynors-platform/auth) : login email/password, JWT RS256 + kid + JWKS, refresh rotation, cookies HttpOnly. Voir Auth · Entitlements · Sécurité — état d'implémentation.

dynors-auth est le service d'émission de tokens de l'écosystème DYNORS. Il fait partie de dynors-platform et constitue la seule source d'authentification pour toutes les applications DYNORS, internes comme satellites.

Périmètre strictement limité à l'identité. Les rôles, permissions et droits d'accès aux produits sont résolus par dynors-entitlements, pas portés dans le JWT. Cette séparation est volontaire : un changement de droit (résiliation, révocation, suspension) doit prendre effet en quelques secondes, ce qui est incompatible avec un JWT à durée de vie longue qui embarquerait les rôles.


1. Positionnement dans l'écosystème

                    ┌──────────────────────────────────┐
                    │   SelebeYone (SLY) — gateway      │
                    │   Route /auth/**  → dynors-auth    │
                    └──────────────┬───────────────────┘
                                   │
                    ┌──────────────▼───────────────────┐
                    │          dynors-auth              │
                    │  ┌────────────────────────────┐  │
                    │  │  Provider email / password  │  │
                    │  │  Provider LDAP              │  │
                    │  │  Token refresh & revocation │  │
                    │  │  Tenant switch              │  │
                    │  └────────────────────────────┘  │
                    │  Émet JWT → cookie HttpOnly       │
                    └──────────────┬───────────────────┘
                                   │ JWT cookie
          ┌────────────────────────┼────────────────────────┐
          ▼                        ▼                        ▼
  SuperGest-API           Mediconnect-API           RAGNAR-API
  dynors-security         dynors-security           dynors-security
  (validation JWT)        (validation JWT)          (validation JWT)

dynors-auth émet. dynors-security valide. Ces deux responsabilités sont distinctes et ne doivent pas être mélangées dans une même app.


2. Repo et déploiement

Attribut Valeur
Repo dynors-platform — module auth
Code app (source-app) auth
Route SLY /auth/**
Port dev 8091
Base de données PostgreSQL dédié dynors_auth_db (comptes, refresh tokens, sessions)

3. Providers d'authentification

3.1 Provider email / password — apps SaaS

Pour les utilisateurs finals des produits DYNORS (SuperGest, Mediconnect, TRACIUM, DYNORS BOOKS, ELISA, DAWALALE, JARAAF en mode client).

  • Stockage credentials dans la DB dynors_auth_db, isolé par tenant.
  • Hash BCrypt (strength 12).
  • Refresh token opaque stocké en base (révocable), posé en cookie HttpOnly séparé.

3.2 Provider LDAP — apps internes

Pour les employés DYNORS (RAGNAR, TAKKU, HEISENBERG, RED et outils internes).

  • Connexion LDAP Active Directory DYNORS.
  • JWT émis avec tenant: "dynors-internal" et aud de l'app interne cible.
  • Mapping groupes LDAP → entitlements ou rôles techniques : voir runbook LDAP (pas de rôles produit embarqués dans le JWT).
  • Aucun stockage de credentials en base (LDAP fait foi).

4. Endpoints

Tous accessibles via SLY — https://sly.dynors.com/auth/**.

POST   /auth/login                   Email/password → JWT cookie
POST   /auth/ldap/login              LDAP → JWT cookie
POST   /auth/refresh                 Renouvelle JWT (à partir refresh token cookie)
POST   /auth/logout                  Révocation + effacement cookies
GET    /auth/me                      Identité utilisateur connecté (lecture JWT validé)
POST   /auth/tenant/switch           Bascule tenant (si user multi-tenant)
GET    /auth/health                  Liveness probe

4.1 POST /auth/login

Request :

{
  "email": "user@client.sn",
  "password": "••••••••",
  "tenant": "teranga-cereales"
}

Response : 200 OK + cookies (prod HTTPS — ADR-0014) :

Set-Cookie: __Host-sid=<JWT>; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600
Set-Cookie: __Secure-rid=<opaque>; HttpOnly; Secure; SameSite=Strict; Path=/auth/refresh; Max-Age=604800

En local (http://localhost) : noms nus sid / rid, Secure désactivé — cf. ADR-0014.

Body :

{
  "sub": "user-uuid",
  "tenant": "teranga-cereales",
  "aud": "b2b",
  "expiresIn": 900
}

Pour récupérer les rôles et permissions de l'utilisateur, le frontend appelle ensuite GET /entitlements/me (voir dynors-entitlements).

4.2 POST /auth/ldap/login

Même contrat que /auth/login mais sans champ tenant (toujours dynors-internal) et credentials vérifiés sur LDAP.

4.3 POST /auth/refresh

Lit le cookie rid (__Secure-rid en prod), vérifie en base, émet un nouveau sid. Aucun body requis — les cookies sont envoyés automatiquement par le navigateur.

4.4 POST /auth/logout

Révoque le refresh token en base, efface les deux cookies (Max-Age=0).

4.5 GET /auth/me

Retourne l'identité décodée du JWT courant (sans accès base) :

{
  "sub": "user-uuid",
  "tenant": "teranga-cereales",
  "aud": "b2b"
}

Utilisé par les frontends Angular au démarrage pour hydrater le store (NgRx, Signal Store) sans décoder le cookie côté client.

Rôles et permissions

/auth/me ne retourne pas les rôles. Le frontend doit ensuite appeler GET /entitlements/me pour récupérer la liste des produits accessibles et les rôles par produit. Voir dynors-entitlements.

4.6 POST /auth/tenant/switch

Pour les utilisateurs rattachés à plusieurs tenants :

Request :

{ "tenant": "nouveau-tenant" }

Response : nouveau JWT cookie valide pour le tenant cible (si l'user y est autorisé).


5. Structure du JWT

Le JWT est volontairement minimal. Il porte l'identité, le rattachement tenant et le realm — rien d'autre.

{
  "iss": "dynors-auth",
  "sub": "user-uuid",
  "tenant": "teranga-cereales",
  "aud": "b2b",
  "exp": 1752003600,
  "iat": 1752000000
}
Claim Description
iss Toujours dynors-auth
sub UUID du sujet (user, customer ou courier selon aud)
tenant Code tenant rattaché — peut être null pour B2C pur (DAWALALE apprenant)
aud Realm — b2b, b2c-<app>, internal, tracking
exp / iat Expiration courte (15-60 min selon realm) / émission (Unix timestamp)

5.1 Realms (aud)

aud Sujet Durée Usage
b2b Employé d'un tenant 15 min SuperGest, Mediconnect cabinet, TRACIUM, BOOKS, JARAAF, ELISA console tenant
b2c-<app> Client final B2C 30 min DAWALALE apprenant, Mediconnect patient, ELISA acheteur
internal Employé DYNORS (LDAP) 60 min RAGNAR, TAKKU, HEISENBERG, RED, back-offices
tracking Accès anonyme éphémère 24h Suivi de livraison ELISA via lien SMS

Pas de roles dans le JWT

Les rôles changent (changement de poste, retrait de droit, suspension). Les mettre dans le JWT crée une fenêtre où l'utilisateur conserve ses droits jusqu'à expiration du token. Les rôles sont résolus à la volée via dynors-entitlements, avec un cache 60s côté apps.

Pas de source-app dans le JWT utilisateur

source-app est un concept service-to-service, porté par InterAppCallService dans le header X-Source-App. Il n'appartient pas au JWT utilisateur. Le mélanger crée des failles d'attribution et des dépendances de déploiement incorrectes.

5.2 Vérification du rattachement produit au login

Au moment du login, dynors-auth interroge dynors-entitlements pour vérifier que le tenant a au moins un produit accessible. Si la réponse est vide (tenant existe mais aucun contrat actif, aucun onboarding complété, aucun grant), le login est refusé avec un message explicite (« Aucun produit DYNORS actif sur ce compte — contactez votre administrateur »). Cela évite d'émettre des JWT pour des comptes orphelins.


6. Validation côté apps — dynors-security

Chaque application inclut dynors-security depuis dynors-core-bom. La validation du JWT est entièrement prise en charge par JwtAuthFilter :

// Ce bean est fourni par dynors-security — ne pas le réécrire
@Bean
public JwtAuthFilter jwtAuthFilter(
        @Value("${dynors.security.jwt.secret}") String secret) {
    return new JwtAuthFilter(secret);
}

JwtAuthFilter : 1. Lit le cookie access (sid / __Host-sid en prod) — priorité au header Bearer uniquement pour intégrations techniques, pas la session navigateur. 2. Valide la signature (RS256 + JWKS), l'expiration, l'issuer (dynors-auth), l'aud. 3. Appelle TenantContext.set(claims.get("tenant")). 4. Ne charge pas les droits produit depuis le JWT — entitlements séparés.

La clé JWT (JWT_SECRET) est partagée entre dynors-auth (signature) et toutes les apps (validation). Elle est stockée dans Vault sous secret/data/dynors/{env}/platform/jwt et injectée via External Secrets Operator (ESO).


7. Intégration frontend Angular

// auth.service.ts
@Injectable({ providedIn: 'root' })
export class AuthService {
  private readonly me$ = this.http.get<UserIdentity>('/auth/me');

  /** Appeler au démarrage de l'app pour hydrater le store */
  loadCurrentUser(): Observable<UserIdentity> {
    return this.me$;
  }

  login(email: string, password: string, tenant: string): Observable<void> {
    return this.http.post<void>('/auth/login', { email, password, tenant });
    // Le cookie est posé automatiquement par le navigateur via Set-Cookie
  }

  logout(): Observable<void> {
    return this.http.post<void>('/auth/logout', {});
  }
}
// app.config.ts — intercepteur cookie (withCredentials)
export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([credentialsInterceptor])
    )
  ]
};

// credentials.interceptor.ts
export const credentialsInterceptor: HttpInterceptorFn = (req, next) =>
  next(req.clone({ withCredentials: true }));

withCredentials: true est obligatoire pour que le navigateur envoie les cookies HttpOnly sur les requêtes cross-origin (frontend supergest.dynors.com → SLY sly.dynors.com).


8. Configuration application.yml de dynors-auth

server:
  port: 8091

spring:
  datasource:
    url: ${AUTH_DB_URL}
    username: ${AUTH_DB_USER}
    password: ${AUTH_DB_PASSWORD}

  ldap:
    urls: ${LDAP_URL:ldap://ldap.dynors.com:389}
    base: ${LDAP_BASE:dc=dynors,dc=com}
    username: ${LDAP_BIND_DN}
    password: ${LDAP_BIND_PASSWORD}

dynors:
  auth:
    jwt-secret: ${JWT_SECRET}
    jwt-expiration-seconds: 3600
    refresh-token-expiration-days: 7
    cookie-domain: .dynors.com          # Valide pour *.dynors.com

  interapp:
    source-app: auth
    gateway-url: ${SLY_BASE_URL:https://sly.dynors.com}
    sly-forward-secret: ${SLY_FORWARD_SECRET}
    sly-signature-window-ms: 30000

9. Variables d'environnement

Variable Source Vault Description
AUTH_DB_URL dynors/{env}/auth/database JDBC URL
AUTH_DB_USER dynors/{env}/auth/database Utilisateur DB
AUTH_DB_PASSWORD dynors/{env}/auth/database Mot de passe DB
JWT_SECRET dynors/{env}/platform/jwt Clé symétrique JWT (partagée avec toutes les apps)
LDAP_URL dynors/{env}/platform/ldap URL LDAP
LDAP_BIND_DN dynors/{env}/platform/ldap DN de liaison LDAP
LDAP_BIND_PASSWORD dynors/{env}/platform/ldap Mot de passe liaison LDAP
SLY_FORWARD_SECRET dynors/{env}/platform/sly Clé HMAC transit SLY

10. Route SLY à déclarer

Dans dynors-platform/selebeyone/application.yml :

dynors:
  sly:
    routes:
      - id: auth
        path: /auth/**
        backend: ${AUTH_BASE_URL:http://dynors-auth:8091}
        strip-prefix: false
        require-sly-pass: false       # Login public, pas de SLY Pass requis
        require-transit-hmac: false   # Requête utilisateur, pas inter-app

11. Checklist d'intégration pour une nouvelle app

  • [ ] NE PAS créer d'endpoint /api/auth/login local dans l'app.
  • [ ] Inclure dynors-security depuis le BOM — ne pas recoder JwtAuthFilter.
  • [ ] Configurer dynors.security.jwt.secret depuis Vault (JWT_SECRET).
  • [ ] Configurer dynors.interapp.* pour les appels inter-app via SLY.
  • [ ] Ajouter withCredentials: true sur tous les appels HTTP Angular.
  • [ ] Le frontend appelle GET /auth/me au démarrage pour hydrater le store.
  • [ ] Aucun token en localStorage ni sessionStorage.
  • [ ] SlyTransitFilter activé si l'app reçoit des appels d'autres apps.

12. Références

Sujet Document
dynors-entitlements — moteur d'autorisation runtime (rôles, droits, produits) dynors-entitlements
Sécurité & Auth — guide développeur Sécurité & Auth
SLY — Gateway et routage Gateway SelebeYone
Routage inter-app (table complète) Routage inter-app
Vault — stratégie secrets Vault — stratégie secrets
Architecture globale (diagramme interactif) Architecture & Interactions