Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ADR-013 - Authentification M2M OAuth pour les services internes

  • Statut : Accepté
  • Date : 2026-06-04

Contexte

Cerops doit permettre à des services non interactifs, comme apps/sentinel, d’appeler l’API interne sans session utilisateur. Ces appels ne doivent pas réutiliser les cookies Better-Auth des applications web, ni dépendre d’un token statique partagé entre services.

Le besoin initial est l’accès de Sentinel aux procédures internes liées aux images satellite. Le mécanisme doit rester extensible à d’autres services internes avec des permissions distinctes.

Décision

Nous utilisons le grant OAuth 2 client_credentials fourni par @better-auth/oauth-provider. Chaque service machine est provisionné comme client OAuth confidentiel avec token_endpoint_auth_method = client_secret_basic.

Le serveur d’autorisation est exposé par Better-Auth sous /api/auth. Le endpoint de token utilisé par les machines est :

POST /api/auth/oauth2/token

Les clients demandent un token avec :

grant_type=client_credentials
resource=<CEROPS_API_AUDIENCE>
scope=<scopes demandes>

L’audience protégée par défaut est http://localhost:3000/rpc. En production, CEROPS_API_AUDIENCE doit correspondre à l’URL publique de l’API RPC.

Provisioning et rotation

Les clients machine sont créés par le script d’administration :

bun run oauth:provision:prod -- --name=sentinel-prod --scopes=satellite:read,satellite:write --admin-session-token=<admin_session_token>

Le script appelle auth.api.adminCreateOAuthClient(...) et stocke les scopes autorisés dans les métadonnées du client. Le client_secret est affiché une seule fois et doit être placé dans le gestionnaire de secrets de l’environnement cible.

La rotation d’urgence utilise :

bun run oauth:rotate:prod -- --client-id=<client_id> --admin-session-token=<admin_session_token>

Cette rotation invalide immédiatement l’ancien secret client. Les JWT déjà émis restent valides jusqu’à leur expiration. Pour une rotation sans interruption, il faut provisionner un second client, déployer ses identifiants, vérifier le renouvellement des JWT, puis désactiver l’ancien client.

Scopes de service

Les scopes M2M supportés sont centralisés dans packages/auth/src/service-scopes.ts :

  • satellite:read
  • processing:write
  • analysis:read
  • analysis:write

Un client ne peut demander que les scopes présents dans ses métadonnées allowedScopes. La fonction assertClientScopesAllowed(...) bloque une demande de token si le client demande un scope non autorisé.

Les procédures ORPC internes déclarent ensuite leurs scopes requis via createServiceProcedure(...). Par exemple, internal.auth.checkM2M exige satellite:read.

Validation côté API

Le contexte API extrait le header HTTP :

Authorization: Bearer <access_token>

Les procédures de service utilisent createServiceProcedure(requiredScopes). Ce middleware :

  • vérifie le JWT avec les clés JWKS exposées par Better-Auth sous /api/auth/jwks ;
  • impose l’issuer BETTER_AUTH_URL + /api/auth ;
  • impose l’audience CEROPS_API_AUDIENCE ;
  • extrait les scopes depuis la claim scope ;
  • rejette la requête en UNAUTHORIZED si le token est absent ou invalide ;
  • rejette la requête en FORBIDDEN si un scope requis manque ;
  • expose context.service avec clientId, subject et scopes.

Les procédures M2M ne dépendent pas de protectedProcedure et ne nécessitent donc pas de session utilisateur. Elles doivent passer par createServiceProcedure(...) pour éviter de mélanger authentification utilisateur et authentification machine.

Fonctionnement du client Sentinel

apps/sentinel utilise CeropsOAuthClient. Au démarrage, Sentinel attend quelques secondes puis appelle :

POST /rpc/internal/auth/checkM2M

Cette sonde échoue le processus si l’authentification M2M n’est pas opérationnelle.

Le client Sentinel :

  • demande un token à /api/auth/oauth2/token avec client_secret_basic ;
  • demande actuellement le scope satellite:read ;
  • met le JWT en cache mémoire ;
  • renouvelle le token avant expiration avec une marge de 60 secondes et un jitter de 15 secondes ;
  • déduplique les renouvellements concurrents ;
  • invalide le cache et retente une fois en cas de réponse 401 ;
  • retente les réponses 429 avec backoff exponentiel borné.

Les variables requises côté Sentinel sont :

  • CEROPS_API_URL
  • CEROPS_API_AUDIENCE
  • CEROPS_OAUTH_CLIENT_ID
  • CEROPS_OAUTH_CLIENT_SECRET

Métadonnées OAuth

Le serveur expose les métadonnées OAuth nécessaires à la découverte :

  • authorization server metadata : /.well-known/oauth-authorization-server/api/auth
  • protected resource metadata pour l’audience RPC
  • JWKS : /api/auth/jwks

Le chemin legacy Better-Auth /api/auth/token est désactivé. Les clients machine doivent utiliser /api/auth/oauth2/token.

Conséquences

  • Les secrets machine sont propres à chaque environnement et ne sont pas codés en dur.
  • Les permissions sont exprimées en scopes, ce qui permet de restreindre un service à son besoin réel.
  • La révocation d’un secret client stoppe les nouveaux tokens, mais pas les JWT déjà émis.
  • Les appels M2M sont indépendants des sessions utilisateur et des cookies httpOnly.
  • L’ajout d’un nouveau service interne nécessite un client OAuth dédié et des scopes explicites.
  • Les erreurs opérationnelles à surveiller sont les 401, 403 et 429 sur les routes internes.

Alternatives considérées

  • Token statique interne : simple à implémenter, mais rotation difficile, absence de scopes fins et risque élevé en cas de fuite.
  • Session utilisateur technique : réutilise l’auth web, mais mélange identité humaine et identité machine, et dépend des cookies.
  • mTLS uniquement : forte garantie d’identité transport, mais plus complexe à opérer et insuffisant pour exprimer des permissions applicatives.
  • Clés API par service : opérationnellement simple, mais moins standard qu’OAuth et moins intégré à Better-Auth/JWKS.