← Tous les articles
26 septembre 20268 min de lecture

Pourquoi un jeton valide ne suffit pas à autoriser une requête

Un access token valide ne garantit pas l’accès. Comprendre la différence entre validation du jeton, droits transportés et autorisation effective.

  • Identity Security
  • JWT
  • RBAC
  • IAM

Dans l’article précédent, j’ai séparé ID token, access token et refresh token. L’access token est celui que le client présente à une API protégée.

Une confusion reste pourtant fréquente :

Le token est valide, donc la requête doit être autorisée.

Non.

Un token peut être correctement signé, venir du bon émetteur, viser la bonne API et ne pas être expiré, tout en étant insuffisant pour l’action demandée.

C’est là que se séparent trois choses : la validation du token, l’information d’autorisation qu’il transporte et la décision d’autorisation effective.

Le problème

Un utilisateur est connecté à une application interne. Son access token arrive sur l’API et passe tous les contrôles techniques.

Puis il appelle :

GET /api/admin/summary

L’API répond :

403 Forbidden

À première vue, cela paraît contradictoire. Si le token est valide, pourquoi refuser la requête ?

Parce que valider un token répond à une autre question que l’autorisation.

La validation cherche à savoir si l’API peut faire confiance au credential reçu. L’autorisation cherche ensuite à savoir si l’identité représentée par ce credential possède le droit d’effectuer cette action sur cette ressource, maintenant.

Un système sain doit donc pouvoir répondre :

Token valide : oui
Accès autorisé : non

Le modèle mental

Je découpe la décision en trois étages :

1. Le token est-il fiable ?
          ↓
2. Que dit-il sur l’appelant ?
          ↓
3. Cet appelant a-t-il le droit demandé ?

Plus concrètement :

Access token
    ↓
Validation
signature / issuer / audience / expiration
    ↓
Claims fiables
sub / rôles / scopes
    ↓
Politique d’autorisation
rôles effectifs / grants / contexte / ressource
    ↓
ALLOW ou DENY

La première étape établit la validité du credential. La deuxième fournit des informations utilisables. La troisième produit la décision de sécurité.

Ce n’est pas parce qu’une donnée est fiable qu’elle suffit à prendre la décision.

Comment ça fonctionne

1. L’API valide d’abord le token

Quand une API reçoit un JWT comme access token, elle vérifie généralement des éléments comme :

signature
issuer
audience
expiration
algorithme attendu
claims obligatoires

Le claim aud désigne les destinataires prévus du JWT. Si l’API n’en fait pas partie, le token doit être rejeté. Le claim exp fixe le moment à partir duquel le JWT ne doit plus être accepté. Ces règles sont notamment définies dans la RFC 7519.

À ce stade, l’API sait essentiellement :

« Ce token a été émis correctement, pour une destination que j’accepte, et il est encore dans sa période de validité. »

Cela ne dit toujours pas :

« L’utilisateur peut administrer cette ressource. »

2. Le token transporte éventuellement de l’information d’autorisation

Un access token peut contenir des éléments utiles à la décision :

{
  "sub": "user-123",
  "aud": "identity-lab-api",
  "realm_access": {
    "roles": ["user"]
  }
}

Selon les systèmes, on trouvera plutôt des scopes, permissions, groupes ou autres claims.

Ces informations peuvent participer à l’autorisation, mais transport d’information et décision ne sont pas synonymes.

Une route peut exiger admin alors que le token ne porte que user. Le token reste valide. Le droit demandé, lui, n’est pas présent.

OAuth prévoit précisément ce type de situation : lorsqu’un bearer token est utilisable mais que les privilèges représentés sont insuffisants, le resource server peut répondre 403 Forbidden avec insufficient_scope. C’est décrit dans la RFC 6750.

3. La décision peut dépendre d’un état extérieur au token

C’est le point le plus intéressant.

Un token est émis à un instant donné. Une décision d’autorisation arrive plus tard, au moment de la requête.

Entre les deux, quelque chose peut changer : un accès est révoqué, une ressource change de propriétaire, un grant expire ou une règle de séparation des tâches s’applique.

Un JWT autoportant ne se réécrit pas après son émission. Si l’API le valide localement sans consulter d’état externe, ses claims restent ceux du moment où il a été émis.

Pour certains droits sensibles, la décision gagne donc à utiliser un état plus frais que le token lui-même.

Exemple concret

Alice utilise une application où le rôle admin peut être accordé après une demande d’accès et une validation manager.

À 10 h 00 :

rôle de base dans le token : user
grant applicatif actif : admin

L’API calcule :

rôles effectifs
=
rôles du token + grants actifs

user + admin
→ accès autorisé

À 10 h 05, un manager révoque le grant admin.

Alice possède toujours le même access token. Sa signature est correcte, son audience n’a pas changé et son expiration n’est pas atteinte.

À 10 h 06 :

rôles du token : user
grants actifs : aucun
rôle requis : admin

→ 403 Forbidden

Le token n’est pas devenu faux.

L’autorisation a changé.

Dans mon lab

C’est la partie centrale de mon Identity Security Lab.

L’API Express applique cette chaîne sur les routes protégées :

authenticate
    ↓
provision
    ↓
withEffectiveRoles
    ↓
requireRole(...)

Le middleware authenticate vérifie l’access token avec jose. Il contrôle notamment :

signature RS256 via le JWKS Keycloak
issuer
audience
expiration
sub

Si cette étape échoue, aucune identité fiable n’est établie et l’API répond 401.

Si elle réussit, la requête continue. Mais je n’utilise volontairement pas les seuls rôles du JWT comme décision finale.

Le middleware suivant calcule :

droits effectifs
=
rôles du token Keycloak
∪
rôles issus des grants ACTIVE en base

Puis requireRole compare cet ensemble au rôle exigé par la route.

Pour :

GET /api/admin/summary

le code impose explicitement :

requireRole("admin")

Un utilisateur peut donc présenter un token entièrement valide et recevoir 403 si admin n’apparaît pas dans ses droits effectifs.

La route a justement été construite pour prouver que le contrôle appartient à l’API, pas au frontend. Masquer le lien /admin améliore l’interface, mais un appel direct avec curl ou les DevTools reste possible. Le backend doit donc refaire le contrôle.

Pourquoi les grants sont relus à chaque requête

Les accès accordés par le workflow sont stockés dans access_grants.

À chaque requête, l’API ne récupère que ceux dont le statut est encore ACTIVE. Ils ne sont pas mis en cache dans la session et ne sont pas recopiés dans un nouveau token.

Pour ces entitlements applicatifs, la révocation devient donc effective dès la requête suivante :

Grant ACTIVE
→ rôle présent

Grant REVOKED
→ rôle absent

Le JWT peut rester cryptographiquement valide pendant ce temps.

Ce choix sépare deux responsabilités : Keycloak porte l’identité et les rôles de base ; l’application garde les accès métier demandés, approuvés et révocables.

Il apporte aussi quelque chose qu’un simple claim de rôle explique mal : pourquoi l’accès existe, qui l’a approuvé et quand.

La limite du modèle

Les grants du lab restent locaux à cette application. Ils ne sont pas propagés à Keycloak et ne deviennent donc pas automatiquement visibles par les autres clients du realm.

Dans un système IGA plus complet, une approbation pourrait déclencher du provisioning vers les systèmes cibles.

Le lab démontre une idée plus précise : la décision d’autorisation peut dépendre d’un état métier plus récent que les claims d’un token déjà émis.

Ce qui peut mal tourner

Vérifier la signature et s’arrêter là

Symptôme : toute personne possédant un token correctement signé atteint une route sensible.

Cause : l’application a confondu authenticité du token et permission d’accès.

Le diagnostic consiste à regarder ce qui se passe après la validation du JWT. Si aucune règle ne contrôle rôle, scope, permission ou contexte, il manque l’autorisation.

Faire confiance uniquement à un droit devenu ancien

Symptôme : un accès révoqué reste utilisable jusqu’au renouvellement ou à l’expiration du token qui le transportait.

La réponse n’est pas forcément d’abandonner les JWT. Il faut décider où doit vivre l’état d’autorisation selon le besoin : tokens courts, introspection, état serveur, grants dynamiques ou combinaison de plusieurs mécanismes.

Contrôler uniquement le frontend

Symptôme : le bouton admin disparaît, mais l’endpoint reste accessible directement.

La question de test est simple :

Puis-je appeler l’API sans passer par l’écran ?

Si oui, le frontend n’est pas une frontière de sécurité.

Confondre 401 et 403

Dans mon API :

401 → aucune identité fiable établie
403 → identité connue, droits insuffisants

C’est cohérent avec HTTP : des credentials valides mais inadéquats pour l’accès demandé peuvent conduire à 403, comme le précise la RFC 9110.

Cette distinction oriente immédiatement le diagnostic : 401 vers le credential, 403 vers la politique d’autorisation.

À retenir

  • Un token valide n’est pas synonyme d’une requête autorisée.
  • Valider le token vérifie que l’API peut faire confiance au credential présenté.
  • Les rôles, scopes ou permissions du token sont des entrées possibles de la décision, pas la décision elle-même.
  • L’autorisation peut dépendre d’un état plus récent : grants actifs, ownership, séparation des tâches ou autre règle métier.
  • Un JWT déjà émis ne reflète pas automatiquement une révocation survenue ensuite.
  • Le contrôle qui fait autorité doit être appliqué par la ressource protégée.
  • 401 et 403 racontent deux problèmes différents.

Le vrai changement de perspective tient dans deux questions :

« Puis-je faire confiance à ce token ? »

et :

« Cette requête doit-elle être autorisée maintenant ? »

Elles ne sont pas équivalentes.

Le prochain article reviendra sur le mécanisme qui permet d’obtenir ces tokens sans exposer directement les credentials au client : Authorization Code Flow avec PKCE, étape par étape.