La délégation par Token Exchange
Un agent agit au nom d'un utilisateur via Token Exchange (RFC 8693) : le jeton délégué garde l'utilisateur comme sujet, attribue l'agent dans le claim act, et ne peut porter que des permissions réduites.
La question centrale de la sécurité des agents est « qui agit vraiment ». Obexal y répond avec OAuth Token Exchange (RFC 8693) : l'agent échange l'access token d'un utilisateur contre un jeton délégué dans lequel l'utilisateur reste le sujet et l'agent est enregistré comme acteur. Les API en aval voient les deux identités, toujours.
Agir au nom d'un utilisateur
Un jeton client_credentials dit « l'agent, pour lui-même ». C'est le mauvais modèle pour le travail au service d'un utilisateur : l'agent agirait avec ses propres permissions générales, et rien en aval ne relierait l'action à la personne qui l'a demandée. Avec Token Exchange, l'agent présente l'access token de l'utilisateur comme preuve du contexte de délégation et reçoit un nouveau jeton qui agit comme l'utilisateur, attribué à l'agent, avec des permissions qui ne peuvent que rétrécir.
La requête d'échange
L'échange est un appel ordinaire à POST /oauth/token. L'agent doit être un client confidentiel, s'authentifier (HTTP Basic ici), et porter le grant token-exchange, sinon la requête est refusée.
| Paramètre (form) | Requis | Valeur |
|---|---|---|
grant_type | oui | urn:ietf:params:oauth:grant-type:token-exchange |
subject_token | oui | L'access token Obexal de l'utilisateur (JWT at+jwt) |
subject_token_type | non | urn:ietf:params:oauth:token-type:access_token, seule valeur supportée |
requested_token_type | non | urn:ietf:params:oauth:token-type:access_token, seule valeur supportée |
scope | non | Sous-ensemble demandé ; défaut : les scopes du subject token |
resource | non | URI absolue du resource server cible (RFC 8707) ; obligatoire si l'agent a une allowlist d'audiences |
curl -sS -X POST https://accounts.obexal.com/oauth/token \
-u "8Zl2vQx0T9hK3mW1s5nDgA:$AGENT_CLIENT_SECRET" \
-d grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
-d subject_token="$USER_ACCESS_TOKEN" \
-d subject_token_type=urn:ietf:params:oauth:token-type:access_token \
-d scope="tickets:read" \
-d resource="https://api.example.eu/tickets"{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6IjIwMjYtMDYifQ...",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"token_type": "Bearer",
"expires_in": 600,
"scope": "tickets:read"
}Remplacez accounts.obexal.com par votre domaine personnalisé si votre organisation en utilise un.
Le jeton délégué : sub et act
Le payload décodé de l'access token délégué :
{
"iss": "https://accounts.obexal.com",
"sub": "b3f1c9d2-5a77-4e10-9c58-2f4a6d8e1b90",
"act": { "sub": "8Zl2vQx0T9hK3mW1s5nDgA" },
"aud": "https://api.example.eu/tickets",
"client_id": "8Zl2vQx0T9hK3mW1s5nDgA",
"scope": "tickets:read",
"tenant": "7c2d1e0f-4b9a-4c3d-8e5f-6a7b8c9d0e1f",
"iat": 1782981000,
"exp": 1782981600
}sub est l'utilisateur : les décisions d'autorisation en aval s'appliquent à lui. act.sub est l'agent : l'attribution n'est jamais perdue. Si le subject token était lui-même un jeton délégué (un agent re-déléguant à un autre agent), l'acteur précédent est imbriqué : "act": {"sub": "agent-B", "act": {"sub": "agent-A"}}. Toute la chaîne reste lisible. L'introspection de jetons (RFC 7662) expose le même claim act : les resource servers obtiennent l'attribution qu'ils valident localement ou par introspection.
Du downscoping, jamais d'escalade
Les scopes du jeton délégué sont l'intersection de toutes les contraintes en jeu :
- le
scopedemandé doit être un sous-ensemble des scopes du subject token, sinoninvalid_scope; - le résultat est intersecté avec les scopes de l'agent, puis avec le plafond de scopes de la politique, puis, pour un agent gouverné, avec les scopes que l'utilisateur a autorisés ;
- si rien ne survit aux intersections, l'échange échoue avec
invalid_scope.
Un agent ne peut donc jamais obtenir à travers un utilisateur plus que ce que l'utilisateur possède, plus que ce qui lui est permis à lui-même, ni plus que ce que sa gouvernance autorise.
Liaison d'audience (RFC 8707)
Sans resource, l'aud du jeton délégué est le client_id de l'agent. Avec resource, le jeton est lié à ce resource server : une API qui vérifie aud (comme elle le doit) rejettera le jeton partout ailleurs. La valeur doit être une URI absolue sans fragment, sinon invalid_target. Si la politique de l'agent définit une allowlist d'audiences, resource devient obligatoire et doit appartenir à la liste (comparaison en forme canonique), sinon invalid_target.
L'autorisation utilisateur des agents gouvernés
Un agent gouverné (créé avec requireConsent: true) ne peut pas agir au nom d'un utilisateur qui ne l'a pas autorisé : l'échange échoue avec invalid_grant. Les utilisateurs gèrent ces autorisations eux-mêmes, via une API self-service authentifiée par session (utilisée par le portail de compte ou votre propre frontend) :
| Méthode et chemin | Effet |
|---|---|
GET /v1/agent-authorizations | Lister les agents que l'utilisateur a autorisés, avec les scopes accordés |
POST /v1/agent-authorizations | Autoriser un agent : {"agentClientId": "...", "scopes": ["tickets:read"]} |
DELETE /v1/agent-authorizations/{clientId} | Révoquer l'autorisation (idempotent) |
Les scopes autorisés doivent être un sous-ensemble des scopes de l'agent, et ils bornent toutes les délégations futures pour cet utilisateur. Les agents internes de confiance (créés sans requireConsent) sautent cette étape.
Garanties et limites
- Isolation par tenant : le subject token doit appartenir à l'organisation de l'agent, sinon
invalid_grant. - Sujets humains uniquement : le sujet doit être un utilisateur actif ; un jeton machine (
subégal auclient_id) est refusé comme sujet. - Vie courte, pas de refresh : le jeton délégué vit 10 minutes par défaut (la politique peut plafonner plus bas) et aucun refresh token n'est émis ; l'agent ré-échange tant que le jeton de l'utilisateur est valide.
- Audité : chaque échange écrit un évènement
oauth.token.exchangeconsignant qui a délégué à quel agent, pour quelle audience et quels scopes ; voir la piste d'audit des délégations. - Révocable : le kill switch refuse les nouveaux échanges et fait rapporter les jetons de l'agent comme inactifs à l'introspection.