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-authest le service d'émission de tokens de l'écosystème DYNORS. Il fait partie dedynors-platformet 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"etaudde 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/loginlocal dans l'app. - [ ] Inclure
dynors-securitydepuis le BOM — ne pas recoderJwtAuthFilter. - [ ] Configurer
dynors.security.jwt.secretdepuis Vault (JWT_SECRET). - [ ] Configurer
dynors.interapp.*pour les appels inter-app via SLY. - [ ] Ajouter
withCredentials: truesur tous les appels HTTP Angular. - [ ] Le frontend appelle
GET /auth/meau démarrage pour hydrater le store. - [ ] Aucun token en
localStoragenisessionStorage. - [ ]
SlyTransitFilteractivé 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 |