CodeGym /Cours /Spring Security /Claims dans le payload JWT : minimum

Claims dans le payload JWT : minimum

Spring Security
Niveau 22, Leçon 2
Disponible

1. JWT comme contrat : facile à transformer en déchet

Lorsqu’un développeur voit JWT pour la première fois, il a très envie de penser ainsi : « Eh bien, le token est signé – donc on peut y mettre tout ce qui pourrait être utile, et ne plus aller en base de données ». En pratique, cela conduit à des tokens-« valises » : on y trouve email, display name, un morceau de profil, des flags internes. Et puis on découvre soudain que la moitié de ces données ne peut pas être exposée, l’autre moitié devient obsolète, et on ne se souvient même plus pourquoi on a ajouté le reste (situation classique : « qui a mis ça ici ? » – « moi » – « pourquoi ? » – « je ne sais pas »).

Dès que le token cesse d’être une chaîne magique et se décompose en header.payload.signature, la question d’ingénierie suivante devient très pratique : que doit exactement résider à l’intérieur du payload pour que la requête puisse être traitée, sans que le token ne se transforme en valise de données superflues.

JWT dans notre cours n’est pas une vitrine « comme on peut », mais un outil qui doit être simple, stable et prévisible. Et pour qu’il le soit, nous devons traiter le payload comme un contrat. Un contrat, c’est lorsque vous pouvez ouvrir la documentation et voir : quels claims existent, ce qu’ils signifient, quel type de valeurs, et lesquels sont vraiment nécessaires au projet Secure Content Platform API.

2. Claim et standard/custom : sens

Techniquement, un claim est simplement une paire clé → valeur à l’intérieur de l’objet JSON payload. Mais du point de vue du développement, un claim est aussi une promesse de sens : si vous avez un claim "sub", alors le monde entier (et votre futur vous dans un mois) s’attend à ce que ce soit « subject », c’est-à-dire le sujet du token. Si vous utilisez "sub" comme « beau nom à l’écran », c’est comme si vous écriviez « date de naissance » dans un passeport et y mettiez votre mème préféré. C’est votre papier, mais les gens autour auront du mal plus tard.

Ici apparaît une distinction utile :

Standard claims – ce sont des champs qui ont déjà une signification généralement acceptée. Il n’est pas obligatoire de tous les utiliser, mais si vous les utilisez, mieux vaut respecter le sens. Cela rend le token plus compatible avec les outils, plus compréhensible pour les collègues et plus facile à déboguer.

Custom claims – ce sont des champs que votre projet invente. Ils sont nécessaires lorsque les champs standard ne suffisent pas pour exprimer votre contexte applicatif. Mais les claims personnalisés, c’est comme les épices : sans eux c’est parfois fade, mais si vous en versez la moitié du pot, le goût deviendra « bizarre », et cela ne se soigne qu’avec des larmes.

Pour faciliter la visualisation, il est pratique de retenir une phrase : les claims standard répondent à « qui/quand/d’où », les claims personnalisés à « ce dont mon application a besoin maintenant ».

3. Claims standard : minimum de base

Il y a plus de claims standard dans la spécification, mais au niveau d’un développeur junior, il est important de savoir lire au moins l’ensemble de base. Nous n’entrons pas dans les détails RFC et ne transformons pas la leçon en encyclopédie, mais nous constatons : ces noms apparaissent le plus souvent, et si vous les voyez, vous devez comprendre le sens général.

Ci-dessous – un tableau « on reconnaît au nom de famille ». Convenons tout de suite : beaucoup de ces claims concernent le temps (iat, nbf, exp). L’important est de les voir pour l’instant comme des marqueurs de frontières temporelles du token : quand il a été émis, à partir de quand il est accepté et quand il cesse de vivre. Cela suffit déjà pour ne pas mélanger le sens des claims avec leur vérification temporelle distincte.

Claim Type de valeur dans payload Sens humain Exemple
sub String Sujet du token : « qui est-ce ? » dans le sens de l’identifiant, et non « comment s’appelle-t-il joliment » "sub": "anna" ou "sub": "user-42"
iss String Qui a émis le token (issuer). Utile pour que les tokens « de notre application » ne soient pas confondus avec ceux des autres "iss": "secure-content-platform"
iat Number Quand a été émis le token "iat": 1773828000
nbf Number Pas avant (not before) : le token « existe » mais ne doit pas encore être accepté "nbf": 1773828000
exp Number Quand il expire : après cela, le token est périmé "exp": 1773828900

On peut ajouter encore quelques claims standard « à l’horizon » pour que vous ne soyez pas effrayé si vous rencontrez : aud (audience) – à qui le token est destiné, jti (JWT ID) – identifiant du token. Ils sont utiles, mais pour notre contrat minimum à ce stade, ils ne sont pas obligatoires.

Le plus important parmi ces cinq est sub. C’est généralement de lui que provient la réponse à la question « qui est l’utilisateur actuel ? » dans une requête stateless. Mais voici la nuance : sub est un identifiant, pas une « représentation humaine ». Si vous y mettez displayName, vous vous compliquez la vie, car displayName peut changer, ne pas être unique et être « Anna (chats) » – et ces identifiants se comportent mal dans le monde backend.

4. Claims personnalisés pour Secure Content Platform API

Maintenant, le plus concret : notre token doit aider notre projet à faire deux choses. Premièrement, retrouver qui est l’utilisateur (principal). Deuxièmement, retrouver quel est son contexte d’accès (rôles/droits), afin que les règles au niveau requête et méthode puissent prendre une décision. Et en même temps, nous nous souvenons des règles owner-based « le sien / l’étranger » : elles nécessitent souvent un identifiant utilisateur stable, et pas seulement « username ».

De là naît un minimum raisonnable de claims personnalisés pour notre projet :

userId (ou le plus court uid) – l’identifiant numérique du compte en base de données, ce fameux UserAccount.id.

roles – la liste des rôles (USER, EDITOR, ADMIN) comme contexte d’accès compact.

Pourquoi pas l’email ? Parce que l’email est une donnée personnelle, et il ne nous est pas nécessaire pour l’autorisation sur chaque requête. Pourquoi pas enabled/locked ? Parce que c’est l’état interne du compte, qui change, et le stocker dans le token comme « vérité » est risqué : le token a été émis dans le passé, alors que l’état du compte vit dans le présent.

Il est utile de formaliser cela dans un petit « token contract » du projet – directement sous forme de tableau. Essayez de penser non seulement « quoi mettre », mais aussi « d’où ça vient », « à quel point c’est stable » et « à quelle fréquence ça change ».

Claim Standard / Personnalisé Type D’où on le tire Modifiable à quel point Pourquoi pour le projet
sub standard String depuis username rarement (généralement) identifiant compréhensible du sujet du token
iss standard String constante de l’application presque jamais distinguer « nos tokens » de ceux des autres
userId personnalisé Long depuis UserAccount.id jamais vérifications owner-based, audit, références aux données
roles personnalisé List<String> depuis les rôles du compte parfois (l’admin modifie les rôles) accès basé sur les rôles, calcul des authorities

Et maintenant un point important : les claims personnalisés ne sont pas des « données utiles en général », ce sont des « données sans lesquelles le projet a du mal / coûte cher à vivre ». Si un champ peut simplement « être utile », mais n’est pas utilisé pour les décisions d’accès, le débogage ou l’audit, il devrait probablement vivre non pas dans le token, mais en base de données et être récupéré de manière classique.

Pour être vraiment clair, voici un exemple de payload (en JSON) qui semble « compact et pertinent » :

{
  "iss": "secure-content-platform",
  "sub": "anna",
  "userId": 42,
  "roles": ["USER", "EDITOR"],
  "iat": 1773828000,
  "exp": 1773828900
}

Ici, il y a des champs standard et quelques personnalisés, et rien de superflu. C’est un bon style de base pour un projet pédagogique : sans marketing, sans magie, sans « instantané de toute la base de données dans le token ».

5. Claims en Java : sans « chaînes partout »

Bien que nous n’écrivions pas encore la génération du token et ne construisions pas le filtre, nous pouvons (et devons) déjà apprendre à penser « en développeur Java » : le contrat du token doit être décrit et typé au moins au niveau « voici les noms des claims, voici leur sens, voici les types attendus ». Sinon, la branche stateless du projet se transforme rapidement en archéologie : chaque classe déterre "sub" avec sa pelle, parfois avec l’erreur "subs".

Constantes pour les noms des claims

La première amélioration simple est de rassembler les noms des claims dans une classe. Oui, ce n’est pas « la sécurité la plus importante au monde », mais c’est une sécurité importante contre le facteur humain (c’est-à-dire de nous-mêmes).

package com.example.securecontent.security.jwt;

// Emplacement unique pour les noms des claims, afin de ne pas multiplier les "chaînes magiques" dans le code.
public final class JwtClaimNames {

    // Standard claims
    public static final String SUBJECT = "sub";
    public static final String ISSUER = "iss";

    // Custom claims de notre projet
    public static final String USER_ID = "userId";
    public static final String ROLES = "roles";

    private JwtClaimNames() {
        // Classe utilitaire : les instances ne sont pas nécessaires
    }
}

C’est amusant, mais ce sont précisément ces solutions « ennuyeuses » qui font gagner des heures de débogage, car l’IDE peut au moins vous avertir que vous avez fait une faute de frappe.

Record comme « documentation dans le code »

La deuxième amélioration est de créer un record qui décrit notre ensemble minimal de claims. Il ne rend pas le token « réel », il rend le modèle de données clair.

package com.example.securecontent.security.jwt;

import java.util.List;

// Contrat minimal du token d'accès au niveau du modèle de données.
public record AccessTokenClaims(
        String sub,        // qui est l'utilisateur (identifiant/subject)
        String iss,        // qui a émis le token (issuer)
        Long userId,       // id stable du compte en base de données
        List<String> roles // rôles qui participent aux vérifications d'accès
) {
}

Notez que dans AccessTokenClaims, il n’y a pas iat/nbf/exp, bien que ce soient des claims standard. Ici, le record décrit l’identité et le contexte d’accès, tandis que les claims temporels sont mieux conservés dans une couche séparée : ils définissent la fenêtre de validité du token, et non le profil de l’utilisateur. Sinon, dans un même modèle, les questions « qui est-ce » et « quand ce token est-il valable » sont mélangées.

Lecture du payload en tant que JSON

Même sans token réel, nous pouvons nous entraîner : le payload est du JSON, et Java peut le lire. Pour les exercices, une chaîne JSON et Jackson, que nous avons déjà dans notre projet, suffisent.

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.Map;

// À des fins pédagogiques, "payload" est pris comme une chaîne JSON ordinaire.
String payloadJson = """
        {"sub":"anna","iss":"secure-content-platform","userId":42,"roles":["USER"]}
        """;

ObjectMapper mapper = new ObjectMapper();

// Lire le JSON dans une Map "brute" pour voir les types réels des valeurs après désérialisation.
Map<String, Object> claims = mapper.readValue(payloadJson, new TypeReference<>() {});

// Récupérer un claim individuel par son nom (dans du code réel, il est préférable de ne pas coder en dur la chaîne).
System.out.println(claims.get("sub")); // anna

Ce morceau de code ne concerne pas la cryptographie JWT ni Spring Security. Il concerne une chose banale mais importante : le payload est des données, et vous devez savoir les lire et les comprendre. Alors JWT cesse d’être une « boîte noire » et devient un contrat ordinaire.

Mini-nuance sur les types

Dans Map<String, Object>, vous rencontrerez presque toujours que les nombres et les listes arrivent comme « des objets quelconques ». Par exemple, userId peut devenir Integer, Long, et dans certains cas même BigInteger – selon la façon dont ils ont été sérialisés et lus. C’est normal, mais dangereux si vous écrivez du code « brutal » avec (Long) claims.get("userId").

Au début, une règle simple aide : considérez d’abord les claims comme du « JSON brut », puis ensuite comme des champs strictement typés. C’est pourquoi un record avec des types clairs est utile comme « objectif », mais le chemin du JSON au record doit être fait avec soin.

6. Erreurs typiques lors du travail avec les claims

Erreur n°1 : utiliser sub comme display name ou email « parce que c’est pratique ».
sub est le sujet du token, c’est-à-dire l’identifiant. Si vous y mettez « Anna Dev » ou un email, vous risquez de cimenter des données personnelles dans chaque requête, et en même temps de rendre l’identifiant instable (l’email et le nom affiché changent parfois). Il est bien plus serein que sub soit quelque chose que vous êtes prêt à considérer comme identifiant principal, par exemple username ou stable user-42.

Erreur n°2 : transformer le payload en « capture de profil » (bio, avatarPath, displayName…).
Même si ces données semblent inoffensives, elles changent. Le token vit pendant un certain temps, et le profil peut être mis à jour une minute plus tard. En conséquence, vous créez un monde où le token commence à mentir simplement parce qu’il est « vieux ». Ce n’est pas un « bug de JWT », c’est une conséquence de la décision de mettre des données mutables dans un contrat immuable.

Erreur n°3 : mettre dans le token ce qui ne peut pas être exposé (ou ce dont on a honte de montrer).
Les mots de passe et passwordHash sont des tabous évidents, mais les débutants mettent parfois des « flags internes » ou des attributs de service qu’ils ne voudraient pas montrer au client. Souvenez-vous de la règle simple de la première leçon du jour : payload est lisible. Traitez-le comme une carte postale, pas comme une lettre scellée.

Erreur n°4 : créer des noms de claims personnalisés chaotiques et ensuite se noyer dans l’incompatibilité.
Aujourd’hui vous avez fait user_id, demain userId, après-demain uid, et une semaine plus tard vous avez trois variantes différentes dans différentes branches du code. Cela semble être un détail, mais ce sont précisément ces détails qui font que la branche stateless « doit être éternellement réparée ». La bonne voie est de fixer une fois les noms des claims et de les extraire dans JwtClaimNames, pour que le projet continue sans surprises.

Erreur n°5 : dupliquer dans le token tout en même temps : les rôles, les authorities, et encore « au cas où » une liste des endpoints disponibles.
Le token doit être compact. Plus vous mettez dans le payload, plus il y a de risques de fuite, d’obsolescence et simplement de « maux de tête » lors de la maintenance. Pour notre projet pédagogique, le minimum raisonnable est userId et roles. Tout le reste doit passer par le principe « sans cela, on ne peut pas prendre une décision d’accès sur la requête ? » Si on peut, alors probablement ce n’est pas nécessaire.

1
Mission
Spring Security, niveau 22, leçon 2
Bloqué
Mappage du payload en record AccessTokenClaims
Mappage du payload en record AccessTokenClaims
1
Mission
Spring Security, niveau 22, leçon 2
Bloqué
Séparation des claims en standard et custom
Séparation des claims en standard et custom
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION