Les rôles, les groupes et le contenu des jetons

Organiser les rôles et les groupes de Keycloak, maîtriser ce qui part dans le jeton d'accès, et savoir expliquer pourquoi Authorization Services reste hors du socle.

Keycloak essentiel Cours 9 sur 17 de Keycloak Academy 40 min 5 questions de quiz Cours suivant : Le raccordement des applications

Sur cette page
  1. Le RBAC de Keycloak : rôles, composites, groupes
  2. Ce qui part dans le jeton
  3. Taille des jetons et jetons légers
  4. Authorization Services : ce qu’il fait, pourquoi on ne le retient pas
  5. À retenir
  6. Natif, extension ou hébergeur
  7. Quiz

Keycloak sait dire qui est l’utilisateur et ce qu’il est : ses rôles, ses groupes, quelques attributs. Il le dit dans un jeton que chaque application lit. Ce cours montre comment le modèle RBAC de Keycloak s’organise, comment contrôler précisément ce qui part dans le jeton, et pourquoi Authorization Services, le moteur d’autorisation fine du produit, n’est en général pas retenu dans un socle IAM mutualisé. Les notions de jeton d’accès, de jeton d’identité et de claim sont supposées connues (voir les fondations OIDC).

Le RBAC de Keycloak : rôles, composites, groupes

Keycloak distingue deux portées de rôles.

  • Un rôle de realm vaut pour tout le realm. Il convient aux statuts transverses : « agent », « prestataire », « administrateur fonctionnel ».
  • Un rôle de client appartient à une application (un client). Il porte un droit propre à cette application : « gestionnaire » dans le portail RH, « consultation » dans l’API de paie. Deux clients peuvent avoir chacun un rôle « admin » sans conflit.

Un rôle composite regroupe d’autres rôles, de realm ou de client. Attribuer « responsable-rh » peut ainsi attribuer en cascade « gestionnaire » du portail et « consultation » de l’API. C’est pratique, mais chaque niveau de composition rend plus difficile la réponse à la question d’audit « pourquoi cet utilisateur a-t-il ce droit ? ».

Les groupes sont hiérarchiques : /rh/paie est un sous-groupe de /rh. On attribue des rôles à un groupe, et chaque membre en hérite. Un membre d’un sous-groupe hérite aussi des rôles de tous les groupes parents. Les groupes portent aussi des attributs, hérités de la même façon.

Enfin, chaque realm a un rôle composite default-roles-<realm> attribué à tout nouvel utilisateur. Il contient par défaut offline_access, uma_authorization et les rôles du client account (accès à la console de compte). C’est lui qui fait apparaître ces rôles dans tous les jetons.

Quel objet pour quel besoin

BesoinObjet KeycloakPourquoi
Un droit qui n’a de sens que dans une applicationRôle de clientIsolé par application, nommé librement, ne pollue pas les autres jetons
Un statut transverse lu par plusieurs applicationsRôle de realmUn seul endroit à maintenir
Un rattachement organisationnel (service, direction)GroupeHiérarchie naturelle, héritage, utile aussi pour la délégation d’administration
Un profil métier qui donne plusieurs droits d’un coupGroupe qui porte les rôles (de préférence à un rôle composite)L’affectation reste lisible : on voit le groupe dans la fiche de l’utilisateur
Une donnée descriptive (matricule, code service)Attribut utilisateur déclaré dans le user profileCe n’est pas un droit ; l’application en fait ce qu’elle veut
Un droit sur un objet précis (« le dossier 4521 »)Rien dans Keycloak : l’applicationTrop fin, trop volatil, inconnu de l’IdP

Conventions de nommage

Les noms de rôles et de groupes deviennent des valeurs dans les jetons et dans le code des applications. Les renommer coûte cher. Quelques règles qui tiennent dans la durée :

  • Rôles de client : un verbe ou une fonction métier courte, en minuscules, sans le nom de l’application (il est déjà dans resource_access). Exemple : gestionnaire, consultation, valideur.
  • Rôles de realm : réservés aux statuts transverses, peu nombreux. Préfixez-les si plusieurs équipes en créent.
  • Groupes : la hiérarchie reflète l’organisation (/direction/service), pas les applications. Pour un groupe « technique » qui ne sert qu’à porter des rôles, un préfixe visible (app-portail-rh-gestionnaires) évite la confusion avec l’organigramme.
  • Ne mettez pas de donnée personnelle ni de donnée sensible dans un nom de rôle ou de groupe : il circule dans chaque jeton.

Ce qui part dans le jeton

Avoir un rôle ne suffit pas pour qu’il apparaisse dans le jeton. Keycloak applique trois filtres successifs.

  1. Les role mappings de l’utilisateur : rôles attribués directement, hérités des groupes, développés depuis les composites.
  2. Le role scope du client : par défaut, l’option « Full scope allowed » (client, onglet Client scopes, dedicated scope, onglet Scope) laisse passer tous les rôles de l’utilisateur. Désactivée, seuls passent les rôles du client lui-même et ceux déclarés explicitement dans son scope.
  3. Les client scopes et leurs protocol mappers : ce sont eux qui écrivent les claims. Sans mapper, rien n’est écrit.

Les client scopes par défaut d’un nouveau client OIDC sont acr, basic, email, profile, roles et web-origins (plus quelques scopes optionnels comme address, phone, offline_access, microprofile-jwt, organization). Trois méritent qu’on s’y arrête :

  • basic (depuis Keycloak 25) écrit sub et auth_time. Avant, sub était ajouté d’office ; il passe maintenant par un mapper qu’on peut voir et régler.
  • roles écrit realm_access.roles et resource_access.<client>.roles, et contient le mapper Audience Resolve, détaillé plus bas.
  • profile et email écrivent les claims d’identité standard.

Voici le jeton d’accès obtenu dans le lab pour Alice, membre de /rh/paie, avec la configuration par défaut :

{
  "iss": "http://localhost:8080/realms/lab-roles",
  "aud": ["api-paie", "account"],
  "sub": "bbf4e7ed-d12f-4a57-a9d4-3052b8956814",
  "typ": "Bearer",
  "azp": "portail-rh",
  "acr": "1",
  "realm_access": {
    "roles": ["agent", "default-roles-lab-roles", "offline_access", "uma_authorization"]
  },
  "resource_access": {
    "portail-rh": { "roles": ["gestionnaire"] },
    "api-paie": { "roles": ["consultation"] },
    "account": { "roles": ["manage-account", "manage-account-links", "view-profile"] }
  },
  "scope": "openid email profile",
  "preferred_username": "alice",
  "email": "alice@academy.local"
}

Le portail reçoit les rôles de l’API de paie et ceux de la console de compte, dont il n’a que faire. C’est l’effet de « Full scope allowed ». Après désactivation et déclaration du seul rôle consultation de l’API dans le scope du portail, plus un mapper de groupes :

{
  "aud": "api-paie",
  "azp": "portail-rh",
  "resource_access": {
    "portail-rh": { "roles": ["gestionnaire"] },
    "api-paie": { "roles": ["consultation"] }
  },
  "groups": ["/rh/paie"]
}

Deux observations. Le rôle de realm agent, hérité de /rh, a disparu : il n’est plus dans le scope du client. Et le claim groups ne liste que l’appartenance directe ; la hiérarchie se lit dans le chemin complet.

Groupes et attributs

Les groupes n’apparaissent dans le jeton que si un mapper Group Membership est ajouté (au client ou à un client scope partagé). Ses options : nom du claim, chemin complet ou nom simple, ajout au jeton d’accès, au jeton d’identité, à l’userinfo, à l’introspection. Le scope optionnel microprofile-jwt contient aussi un claim nommé groups, mais il y place les rôles de realm, pas les groupes. Ce piège de nommage revient souvent.

Les attributs utilisateur passent par un mapper User Attribute. L’attribut doit être déclaré dans le user profile, sinon il n’est ni éditable ni garanti.

L’audience

Le claim aud dit à qui le jeton est destiné. Une API qui ne vérifie pas aud accepte un jeton émis pour une autre application du même realm. RFC 9700 recommande de restreindre l’audience des jetons d’accès.

Keycloak remplit aud de deux façons :

  • Audience Resolve (dans le scope roles) ajoute automatiquement tout client dont au moins un rôle de client figure dans le jeton. Dans l’exemple, api-paie est dans aud parce que le rôle consultation y est.
  • Audience (mapper à ajouter) inscrit un client précis (« Included Client Audience ») ou une valeur libre (« Included Custom Audience »). C’est la solution quand l’API n’a pas de rôles de client, ou quand un composant comme oauth2-proxy exige que son propre identifiant figure dans aud.

Le jeton d’identité, lui, a toujours pour audience le client qui l’a demandé.

Taille des jetons et jetons légers

Un jeton JWT voyage dans l’en-tête Authorization. Les reverse proxies et serveurs applicatifs limitent la taille des en-têtes, souvent autour de 8 Ko. Un utilisateur membre de dizaines de groupes, avec « Full scope allowed » activé, peut dépasser cette limite : l’erreur apparaît alors loin de Keycloak (414, 431 ou 502 selon le composant), ce qui la rend difficile à diagnostiquer.

Trois leviers, du plus simple au plus structurant :

  1. Restreindre le scope des clients et retirer les mappers inutiles (groupes, attributs que personne ne lit).
  2. Déplacer des claims vers l’endpoint userinfo plutôt que dans le jeton d’accès.
  3. Utiliser des jetons d’accès légers (lightweight access tokens, depuis Keycloak 24).

Un jeton léger ne contient que des claims techniques (exp, iat, jti, iss, typ, azp, sid, scope, et cnf s’il y a lieu) et ceux des mappers dont l’option « Add to lightweight access token » est activée. On l’active par client avec l’option « Always use lightweight access token » (onglet Advanced), ou de façon conditionnelle avec l’executor use-lightweight-access-token d’une client policy. Voici le jeton du portail une fois l’option activée :

{
  "exp": 1793876196,
  "iat": 1793875896,
  "jti": "onltro:f39cbe3d-5730-6718-60fa-d08643b96cf9",
  "iss": "http://localhost:8080/realms/lab-roles",
  "typ": "Bearer",
  "azp": "portail-rh",
  "sid": "b091077f-c3d3-1965-3e96-af8e5bc500b0",
  "scope": "openid profile email"
}

Plus de sub, plus de aud, plus de rôles. L’API récupère ces informations en appelant l’endpoint d’introspection (/realms/<realm>/protocol/openid-connect/token/introspect), qui renvoie tous les claims marqués « Add to token introspection ». Le compromis est clair : des jetons courts et sans donnée personnelle, contre un appel réseau à Keycloak pour chaque validation (ou un cache côté API) et une API qui doit disposer d’identifiants pour introspecter.

SituationJeton completJeton léger
API qui valide le JWT localement, sans appel à KeycloakOuiNon, sauf à marquer les claims utiles
Jeton exposé à un client tiers qui ne doit pas lire de données personnellesÀ éviterOui
Nombreux groupes ou rôles, en-têtes trop grosAprès réduction du scopeOui
API à très fort trafic, latence critiqueOuiSeulement avec un cache d’introspection

Authorization Services : ce qu’il fait, pourquoi on ne le retient pas

Authorization Services est un moteur d’autorisation fine intégré à Keycloak. On l’active sur un client confidentiel (option « Authorization »). Il ajoute :

  • des ressources (un objet protégé, par exemple « dossier de paie ») et des scopes d’autorisation (« lire », « valider ») ;
  • des politiques : par rôle, par groupe, par utilisateur, par client, par horaire, par expression régulière, agrégées, ou par script JavaScript déployé dans une extension ;
  • des permissions qui relient ressources ou scopes à des politiques, avec une stratégie de décision (unanime, affirmative, consensus) ;
  • un endpoint qui délivre un RPT (Requesting Party Token) contenant les permissions accordées, et le support de UMA 2.0 pour le partage de ressources entre utilisateurs ;
  • un policy enforcer côté application, qui intercepte les requêtes et interroge Keycloak.

C’est puissant, et c’est justement le problème dans un socle mutualisé :

CritèreRBAC + claims (retenu)Authorization Services
Où vit la règleRôle grossier dans Keycloak, règle fine dans l’applicationDans Keycloak, par application
Couplage applicatifLire un JWT standardPolicy enforcer ou appels spécifiques à Keycloak (RPT, UMA)
Portabilité vers un autre IdPBonne : claims standardFaible : modèle propre à Keycloak
Qui maintient les règlesL’équipe applicative, avec son codeL’équipe IAM, qui doit connaître les objets métier de chaque application
Prestataires externesSavent fairePeu le connaissent
Données nécessaires à la décisionCelles de l’applicationDoivent être connues de Keycloak ou transmises

La position à défendre : Keycloak fournit l’identité, des rôles d’application grossiers, l’appartenance aux groupes et l’audience. Chaque application décide de l’accès à ses propres objets. On reste dans le cadre natif de Keycloak sans en faire un moteur de règles métier. Si un besoin d’autorisation centralisée apparaît plus tard, il se traite comme un projet à part, pas comme une option du socle.

Rôles, groupes et inspection des jetons

30 min

Installer le lab
  1. Démarrez le lab (docker compose up -d dans keycloak-academy-lab/) puis, toujours dans keycloak-academy-lab/, connectez kcadm.sh :
KC="docker compose exec -T keycloak /opt/keycloak/bin/kcadm.sh"
$KC config credentials --server http://localhost:8080 --realm master --user admin --password admin
  1. Créez le realm, un portail et une API, avec un rôle de realm et un rôle de client sur chacun :
$KC create realms -s realm=lab-roles -s enabled=true
$KC create clients -r lab-roles -s clientId=portail-rh -s secret=lab-secret \
  -s directAccessGrantsEnabled=true -s 'redirectUris=["http://localhost:3000/*"]'
$KC create clients -r lab-roles -s clientId=api-paie -s standardFlowEnabled=false
$KC create roles -r lab-roles -s name=agent
PORTAIL=$($KC get clients -r lab-roles -q clientId=portail-rh --fields id --format csv --noquotes)
API=$($KC get clients -r lab-roles -q clientId=api-paie --fields id --format csv --noquotes)
$KC create clients/$PORTAIL/roles -r lab-roles -s name=gestionnaire
$KC create clients/$API/roles -r lab-roles -s name=consultation
  1. Créez le groupe /rh et son sous-groupe /rh/paie, attribuez les rôles, puis créez Alice dans le sous-groupe :
RH=$($KC create groups -r lab-roles -s name=rh -i)
PAIE=$($KC create groups/$RH/children -r lab-roles -s name=paie -i)
$KC add-roles -r lab-roles --gid $RH --rolename agent
$KC add-roles -r lab-roles --gid $RH --cclientid portail-rh --rolename gestionnaire
$KC add-roles -r lab-roles --gid $PAIE --cclientid api-paie --rolename consultation
U=$($KC create users -r lab-roles -s username=alice -s enabled=true -s email=alice@academy.local \
  -s firstName=Alice -s lastName=Martin -s emailVerified=true -i)
$KC set-password -r lab-roles --userid $U --new-password password
$KC update users/$U/groups/$PAIE -r lab-roles -n
  1. Dans la console (http://localhost:8080/admin, realm lab-roles), ouvrez Clients, portail-rh, onglet Client scopes, sous-onglet Evaluate. Choisissez Alice et affichez « Generated access token ». Vérifiez que agent et gestionnaire sont hérités de /rh, et repérez les rôles du client account.

  2. Obtenez un vrai jeton et décodez-le. Le grant password (direct access grant) ne sert ici qu’à l’inspection ; il est à proscrire en production.

curl -s http://localhost:8080/realms/lab-roles/protocol/openid-connect/token \
  -d grant_type=password -d client_id=portail-rh -d client_secret=lab-secret \
  -d username=alice -d password=password -d scope=openid \
  | python3 -c "import json,sys,base64; t=json.load(sys.stdin)['access_token']; p=t.split('.')[1]; print(len(t)); print(json.dumps(json.loads(base64.urlsafe_b64decode(p+'='*(-len(p)%4))),indent=2))"
  1. Restreignez le scope du portail, déclarez le seul rôle utile de l’API et ajoutez un mapper de groupes :
$KC update clients/$PORTAIL -r lab-roles -s fullScopeAllowed=false
ROLE=$($KC get clients/$API/roles/consultation -r lab-roles --fields id,name)
$KC create clients/$PORTAIL/scope-mappings/clients/$API -r lab-roles -b "[$ROLE]"
$KC create clients/$PORTAIL/protocol-mappers/models -r lab-roles \
  -s name=groupes -s protocol=openid-connect -s protocolMapper=oidc-group-membership-mapper \
  -s 'config."claim.name"=groups' -s 'config."full.path"=true' \
  -s 'config."access.token.claim"=true' -s 'config."introspection.token.claim"=true'
  1. Relancez la commande de l’étape 5. Comparez la taille du jeton, la disparition de agent et des rôles account, la valeur de aud et le claim groups.

  2. Activez le jeton léger, relancez l’étape 5, puis introspectez le jeton obtenu (remplacez <jeton>) :

$KC update clients/$PORTAIL -r lab-roles -s 'attributes."client.use.lightweight.access.token.enabled"=true'
curl -s -u portail-rh:lab-secret -d token=<jeton> \
  http://localhost:8080/realms/lab-roles/protocol/openid-connect/token/introspect | python3 -m json.tool
  1. Notez pour vous-même : quelles applications de votre périmètre valident les jetons localement, et lesquelles pourraient introspecter ? La réponse décide si le jeton léger est une option réaliste.

À retenir

  • Rôles de client pour les droits d’une application, rôles de realm pour les statuts transverses, groupes pour l’organisation et l’affectation en masse.
  • Un rôle ne part dans le jeton que s’il passe le scope du client et qu’un mapper l’écrit. Désactivez « Full scope allowed ».
  • Faites vérifier aud par chaque API ; Keycloak le remplit via Audience Resolve ou un mapper Audience.
  • Le jeton léger réduit taille et exposition des données, au prix de l’introspection.
  • Authorization Services existe et fonctionne, mais il déplace la logique métier dans l’IdP et couple les applications à Keycloak : on le connaît pour pouvoir expliquer pourquoi on ne le prend pas.

Natif, extension ou hébergeur

Natif : fourni par Keycloak, à configurer. Extension : demande du code à maintenir (un SPI, le mécanisme de plugin de Keycloak). Hébergeur : dépend de la plateforme qui héberge Keycloak, à vérifier avec elle.

CapacitéVerdictPrécision
Rôles de realm, rôles de client et rôles compositesNatifModèle RBAC de base, administrable en console, par kcadm.sh ou par l'Admin REST API.
Groupes hiérarchiques et héritage des rôlesNatifUn membre d'un sous-groupe hérite des rôles de tous les groupes parents.
Rôles, groupes, attributs et audience dans le jetonNatifPilotés par les client scopes et les protocol mappers, sans code.
Jeton d'accès léger et introspectionNatifOption « Always use lightweight access token » par client, ou executor de client policy ; disponible depuis Keycloak 24.
Claim calculé par une règle métier (appel d'API, transformation complexe)ExtensionDemande un protocol mapper développé (SPI), à maintenir à chaque montée de version.
Autorisation fine centralisée (Authorization Services, UMA)NatifPrésent dans le produit, mais il impose un point d'application des politiques côté application ; c'est ce couplage qui justifie de ne pas le retenir dans le socle.

Vérifier ce que vous avez retenu

5 questions

  1. Une API reçoit des jetons de six applications clientes. Elle ne doit accepter que ceux qui lui sont destinés. Quel réglage Keycloak lui permet de faire ce contrôle de façon fiable ?

  2. Les jetons d'un portail dépassent 8 Ko : l'utilisateur est membre de nombreux groupes et le client a « Full scope allowed » activé. Quelle première mesure est la plus juste ?

  3. Vous activez « Always use lightweight access token » sur un client. Que doit faire l'API qui consomme ces jetons pour obtenir l'identité et les rôles ?

  4. Un métier veut que la règle « un gestionnaire ne voit que les dossiers de son service » soit gérée dans Keycloak. Quelle position d'architecture défendre ?

  5. Alice est membre du sous-groupe /rh/paie. Le groupe /rh porte le rôle de client « gestionnaire ». Le mapper « Group Membership » est activé avec le chemin complet. Que contient le jeton ?