TutorielGuides

Délégation entre API avec le token exchange

Sur cette page

Une API qui reçoit le jeton d’un utilisateur a souvent besoin d’en appeler une autre au nom du même utilisateur. Faire suivre le jeton reçu est tentant, mais c’est une mauvaise idée : il a été émis pour la première API, avec ses scopes à elle, et l’API suivante ne saurait pas qu’un intermédiaire est passé par là. Le token exchange (RFC 8693) règle la question : la première API échange auprès de TOSIAM le jeton de l’utilisateur contre un nouveau jeton, pour le même utilisateur, avec le scope que l’API suivante attend, et se déclare comme acteur. L’API suivante voit qui est l’utilisateur (sub) et quelle API agit pour lui (act).

Le code est dans le dépôt tosiam-samples, dossier tosiam-token-exchange : une boutique en ligne de commande et deux API, en Node.js sans dépendance.

  1. Boutique vers API des commandes GET /commandes avec le jeton de dduck (scope commandes.lecture)
  2. API des commandes vers TOSIAM Introspection du jeton reçu
  3. API des commandes vers TOSIAM Son propre jeton (client_credentials) : l’acteur
  4. API des commandes vers TOSIAM Token exchange : jeton de dduck + jeton de l’acteur, scope stock.lecture
  5. TOSIAM vers API des commandes Nouveau jeton : sub = dduck, act = api-commandes
  6. API des commandes vers API du stock GET /stock avec le nouveau jeton
  7. API du stock vers TOSIAM Introspection : sub et act
  8. API du stock vers API des commandes Le stock, « consulté par api-commandes pour le compte de dduck »
  9. API des commandes vers Boutique Les commandes et le stock
La boutique n’a qu’un jeton pour les commandes ; l’API des commandes obtient, au nom de l’utilisateur, un jeton pour le stock.

Étape 1 : les clients et l’acteur autorisé#

L’exemple crée trois clients dans le royaume ref :

Faites défiler le tableau
ClientTypeGrantsScopes
boutique-cliPublicdevice flow, refresh_tokencommandes.lecture
api-commandesConfidentielclient_credentials, token exchangestock.lecture, am-introspect-all-tokens
api-stockConfidentielclient_credentialsam-introspect-all-tokens

Le client de la boutique est celui du tutoriel Connecter un appareil (device flow), avec le scope commandes.lecture. Le client qui compte ici est api-commandes, créé dans Clients, Créer un agent, type Confidentiel. Onglet Général de sa fiche :

Fiche du client api-commandes, onglet Général : type de client Confidentiel, scopes stock.lecture et am-introspect-all-tokens Fiche du client api-commandes, onglet Général : type de client Confidentiel, scopes stock.lecture et am-introspect-all-tokens
Type de client Confidentiel (1), scope stock.lecture (2) et scope am-introspect-all-tokens (3).
  • Type de client : Confidentiel. Un client public ne peut pas échanger de jetons.
  • stock.lecture : le scope qu’api-commandes demandera à l’échange. Le scope d’un échange est borné par les scopes du client qui échange : sans lui, l’échange répond invalid_scope.
  • am-introspect-all-tokens : pour introspecter le jeton de la boutique, émis à un autre client.

Onglet Avancé, Types d’autorisation :

Fiche du client api-commandes, onglet Avancé : types d'autorisation Client Credentials et Token Exchange cochés Fiche du client api-commandes, onglet Avancé : types d'autorisation Client Credentials et Token Exchange cochés
Client Credentials (1), pour le jeton de l’acteur, et Token Exchange (2).

Enfin, le fournisseur OAuth2 du royaume tient la liste des acteurs autorisés : les clients qui peuvent échanger des jetons et se déclarer acteurs. Dans Services, OAuth2 Provider, champ « Liste des acteurs autorisés pour token_exchange », ajoutez clt|api-commandes (usr| pour un utilisateur, grp| pour les membres d’un groupe) :

Fournisseur OAuth2 : liste des acteurs autorisés pour token_exchange avec la valeur clt|api-commandes Fournisseur OAuth2 : liste des acteurs autorisés pour token_exchange avec la valeur clt|api-commandes
La valeur clt|api-commandes dans les acteurs autorisés (1).

Sans elle, l’échange répond invalid_grant « This client is not authorized as actor for token-exchange ». Le script de l’exemple l’ajoute aux valeurs existantes (ssoadm set-realm-svc-attrs --append) et delete.sh ne retire qu’elle.

Étape 2 : vérifier le jeton reçu#

Chaque API vérifie le jeton qu’elle reçoit par introspection, avec son propre client confidentiel. Elle exige un access token actif (TOSIAM introspecte aussi les refresh tokens) portant le scope attendu. Le jeton de la boutique a été émis à boutique-cli, celui que reçoit l’API du stock à api-commandes : pour lire des jetons émis à d’autres clients, le client de l’API a besoin du scope am-introspect-all-tokens, sinon TOSIAM répond {"active": false}.

javascript· lib/http.js
let info;
try {
  info = await tosiam.introspect(token);
} catch (e) {
  send(res, 503, { error: 'tosiam_indisponible', message: e.message });
  return undefined;
}
// TOSIAM introspects refresh tokens too: only an access token may be used as a bearer token
if (!info.active || info.token_type !== 'access_token') {
  send(res, 401, { error: 'jeton_invalide', message: 'Jeton inconnu, expiré, révoqué, ou qui n\'est pas un access token' },
    challenge('invalid_token'));
  return undefined;
}
const scopes = String(info.scope ?? '').split(/\s+/);
if (!scopes.includes(scope)) {
  send(res, 403, { error: 'scope_insuffisant', scopeAttendu: scope }, challenge('insufficient_scope'));
  return undefined;
}

Le jeton de la boutique, vu par l’API des commandes :

json
{
  "active": true,
  "scope": "commandes.lecture",
  "client_id": "boutique-cli",
  "user_id": "dduck",
  "token_type": "access_token",
  "exp": 1791639326,
  "sub": "dduck",
  "iss": "http://localhost:8080/tosiam/oauth2/ref"
}

Étape 3 : l’échange#

L’API des commandes a un jeton commandes.lecture ; l’API du stock exige stock.lecture. L’API des commandes demande d’abord son propre jeton (client_credentials), qui la représente comme acteur, puis échange le jeton de l’utilisateur :

http
POST /tosiam/oauth2/ref/access_token
Authorization: Basic base64(api-commandes:api-commandes-secret)

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<jeton de dduck>
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&actor_token=<jeton d'api-commandes>
&actor_token_type=urn:ietf:params:oauth:token-type:access_token
&scope=stock.lecture
json
{
  "access_token": "…",
  "refresh_token": "…",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "token_type": "Bearer",
  "expires_in": 3599
}

Dans le code, le jeton de l’acteur est partagé par les requêtes tant qu’il est valable ; si TOSIAM ne l’accepte plus (révoqué, TOSIAM réinstallé), il est renouvelé une fois. Le jeton échangé est révoqué dès que l’API du stock a répondu : il n’a servi qu’à un appel.

javascript· api-commandes/app.js
/** Exchanges the user's token; a cached actor token TOSIAM no longer accepts is renewed once. */
async function exchange(subjectToken) {
  const current = actorToken();
  const fresh = current.until === Infinity; // obtained for this request, or by one still in flight
  try {
    const { accessToken } = await current.promise;
    return await tosiam.exchange({ subjectToken, actorToken: accessToken, scope: 'stock.lecture' });
  } catch (e) {
    if (fresh) throw e;
    // Revoked, or TOSIAM reset: the cached actor token is dropped, and the exchange tried again
    if (actor === current) actor = undefined;
    const { accessToken } = await actorToken().promise;
    return tosiam.exchange({ subjectToken, actorToken: accessToken, scope: 'stock.lecture' });
  }
}

Puis, pour chaque requête (extrait, sans les journaux) :

javascript· api-commandes/app.js
let exchanged;
try {
  exchanged = await exchange(auth.token);
} catch (e) {
  return send(res, 502, { error: 'echange_refuse', message: e.message });
}
try {
  // … appel de l'API du stock avec exchanged.accessToken ; 502 si elle est injoignable, refuse le jeton
  //   ou répond autre chose que du JSON ; sinon les commandes et le stock
} finally {
  await tosiam.revoke(exchanged.refreshToken ?? exchanged.accessToken).catch(() => {});
}

TOSIAM contrôle l’échange :

Faites défiler le tableau
ContrôleSinon
Le client qui échange est confidentiel, a le grant token exchange et figure dans les acteurs autorisés (clt|api-commandes)invalid_grant « This client is not authorized as actor for token-exchange »
Un actor_token d’un autre client ou d’un utilisateur est déclaré dans les acteurs autorisés (clt|, usr|, grp|)authorization_declined
Le scope demandé est dans les scopes du client qui échange ; il peut différer de celui du jeton échangéinvalid_scope
openid seulement si le jeton échangé le porteinvalid_scope
audience n’est pas pris en chargeinvalid_target

Le détail des paramètres est dans Grant types.

Étape 4 : l’API du stock lit sub et act#

L’API du stock ne connaît rien de l’échange : elle vérifie le jeton par introspection, comme n’importe quel autre. La réponse dit pour qui (sub) et par qui (act.sub) :

json
{
  "active": true,
  "scope": "stock.lecture",
  "client_id": "api-commandes",
  "user_id": "dduck",
  "token_type": "access_token",
  "exp": 1791639326,
  "sub": "dduck",
  "iss": "http://localhost:8080/tosiam/oauth2/ref",
  "act": {
    "sub": "api-commandes"
  }
}
javascript· api-stock/app.js
const { sub, act } = auth.info;
// act.sub: the API that acts for the user (RFC 8693) ; none for a direct call
const actor = act?.sub ?? null;
send(res, 200, { articles: STOCK, pourLeCompteDe: sub, consultePar: actor });

Une API peut s’appuyer sur act pour tracer les délégations, ou pour restreindre ce qu’un intermédiaire a le droit de faire au nom de l’utilisateur. Après deux échanges successifs, act s’imbrique : {"sub": "<dernier acteur>", "act": {"sub": "<acteur précédent>"}}.

Étape 5 : lancer et tester#

bash
cd tosiam-token-exchange
./up.sh
node boutique/boutique.js
sortie
Boutique — TOSIAM http://localhost:8080/tosiam, royaume ref, API des commandes http://localhost:8085

Sur votre téléphone ou votre ordinateur, ouvrez : http://localhost:8080/tosiam/oauth2/ref/device/user
et saisissez le code : PXCRHZA3
Lien direct : http://localhost:8080/tosiam/oauth2/ref/device/user?user_code=PXCRHZA3

En attente de l'utilisateur (code valable 5 min)…

Ouvrez l’adresse, connectez-vous avec dduck / Donald-Duck-2026, saisissez le code. L’écran de consentement nomme la boutique (1) et ce qu’elle demande (2) ; choisissez Autoriser (3) :

Écran de consentement : application Boutique, information demandée Lecture de vos commandes, boutons Refuser et Autoriser Écran de consentement : application Boutique, information demandée Lecture de vos commandes, boutons Refuser et Autoriser
Le nom du client (1), la description du scope commandes.lecture (2) et Autoriser (3). L’utilisateur ne consent qu’aux commandes : le stock est lu par délégation.
sortie
Commandes de dduck :
  C-1001  Parapluie jaune × 1
  C-1002  Ciré marin × 2
Stock (consulté par api-commandes pour le compte de dduck) :
  Parapluie jaune : 12
  Bottes en caoutchouc : 3
  Ciré marin : 0
Jeton révoqué.

Les journaux des API (.tosiam/api-commandes.log, .tosiam/api-stock.log) nomment l’utilisateur et l’acteur, jamais un jeton :

sortie
2026-10-10T12:31:35.841Z GET /stock → 200 pour dduck, par api-commandes

En cas de problème#

Faites défiler le tableau
SymptômeCause probable
invalid_grant « This client is not authorized as actor for token-exchange »clt|api-commandes absent des acteurs autorisés du fournisseur, ou client public
authorization_declined « The actor does not have the permission … »L’actor_token est celui d’un autre client ou d’un utilisateur qui n’est pas déclaré dans les acteurs autorisés
invalid_scope à l’échangeLe scope demandé n’est pas dans les scopes du client qui échange, ou openid absent du jeton échangé
invalid_target à l’échangeParamètre audience envoyé : il n’est pas pris en charge
L’API répond 401 jeton_invalide à un jeton valideSon client n’a pas le scope am-introspect-all-tokens : TOSIAM répond active: false pour les jetons des autres clients
act absent de l’introspectionÉchange sans actor_token : le jeton porte l’utilisateur, pas l’API qui agit
L’utilisateur a validé (« Done! ») mais la boutique attend encoreDéfaut connu : une interrogation de la boutique au même instant que la validation peut l’effacer ; relancez la boutique

En production#

  • Un jeton par API : l’API intermédiaire ne fait pas suivre le jeton reçu ; elle échange un jeton dont le scope vise l’API suivante, et seulement celui-là.
  • Déclarez l’acteur : avec actor_token, l’API finale sait quelle API agit pour l’utilisateur et peut le tracer ou le restreindre.
  • Limitez les acteurs autorisés au strict nécessaire : chaque client de la liste peut obtenir, au nom des utilisateurs dont il reçoit les jetons, des jetons pour ses propres scopes.
  • Des jetons échangés de courte durée, révoqués après usage, et jamais journalisés.
  • Secrets des clients dans le gestionnaire de secrets de la plateforme, pas dans le code ; ou une authentification par clé (private_key_jwt) ou par certificat (mTLS).

Pour aller plus loin#

Mis à jour le