Supercharge l’authentification Java avec les jetons Web JSON (JWT)

Supercharge l'authentification Java avec les jetons Web JSON (JWT)

Vous êtes prêt à mettre en place une authentification sécurisée dans votre application Java ou vous vous battez pour elle? Vous n'êtes pas sûr des avantages de l'utilisation de jetons (et en particulier des jetons Web JSON), ni de la manière dont ils devraient être déployés? Je suis ravi de répondre à ces questions, et plus encore, pour vous dans ce didacticiel!

Avant de plonger dans les jetons Web JSON (JWTs) et lesJJWT library (créés par le CTO de Stormpath, Les Hazlewood et maintenus par uncommunity of contributors), couvrons quelques notions de base.

1. Authentification vs. Authentification par jeton

L'ensemble de protocoles qu'une application utilise pour confirmer que l'identité de l'utilisateur est l'authentification. Les applications ont traditionnellement conservé l'identité par le biais de cookies de session. Ce paradigme repose sur le stockage des ID de session côté serveur, ce qui oblige les développeurs à créer un stockage de session unique et spécifique au serveur, ou mis en œuvre en tant que couche de stockage de session complètement séparée.

L'authentification par jeton a été développée pour résoudre les problèmes que les identifiants de session côté serveur n'ont pas fait et n'ont pas pu. Tout comme l'authentification traditionnelle, les utilisateurs présentent des informations d'identification vérifiables, mais se voient désormais attribuer un ensemble de jetons au lieu d'un identifiant de session. Les informations d'identification initiales peuvent être la paire nom d'utilisateur / mot de passe standard, les clés d'API ou même des jetons d'un autre service. (La fonction d’authentification de la clé d’API de Stormpath en est un exemple.)

1.1. Pourquoi des jetons?

Très simplement, l’utilisation de jetons au lieu d’ID de session peut réduire la charge de votre serveur, rationaliser la gestion des autorisations et fournir de meilleurs outils pour la prise en charge d’une infrastructure distribuée ou en nuage. Dans le cas de JWT, ceci est principalement accompli grâce à la nature sans état de ces types de jetons (plus de détails ci-dessous).

Les jetons offrent une grande variété d'applications, notamment: les schémas de protection contre la falsification de requêtes intersites (CSRF), les interactionsOAuth 2.0, les identifiants de session et (dans les cookies) comme représentations d'authentification. Dans la plupart des cas, les normes ne spécifient pas de format particulier pour les jetons. Voici un exemple deSpring Security CSRF token typique sous forme HTML:

Si vous essayez de publier ce formulaire sans le bon jeton CSRF, vous obtenez une réponse d'erreur, et c'est l'utilité des jetons. L'exemple ci-dessus est un jeton «muet». Cela signifie qu'il n'y a pas de signification inhérente à tirer du jeton lui-même. C’est également à ce niveau que les JWT font une grande différence.

Lectures complémentaires:

Utilisation de JWT avec Spring Security OAuth

Guide d'utilisation des jetons Web JSON avec signature symétrique et asymétrique dans Spring Security OAuth.

Read more

Spring REST API + OAuth2 + Angular

Apprenez à configurer OAuth2 pour une API Spring REST et à l'utiliser à partir d'un client angulaire.

Read more

OAuth2 pour une API REST de printemps - Gestion du jeton d'actualisation dans AngularJS

Nous avons appris comment stocker le jeton d'actualisation dans une application cliente AngularJS, comment actualiser un jeton d'accès expiré et comment exploiter le proxy Zuul.

Read more

2. Que contient un JWT?

Les JWT (prononcés par «jots») sont des chaînes sécurisées pour les URL, encodées et signées de manière cryptographique (parfois cryptées) pouvant être utilisées comme jetons dans diverses applications. Voici un exemple d'un JWT utilisé comme jeton CSRF:

Dans ce cas, vous pouvez voir que le jeton est beaucoup plus long que dans notre exemple précédent. Comme nous l'avons vu précédemment, si le formulaire est soumis sans le jeton, vous obtenez une réponse d'erreur.

Alors, pourquoi JWT?

Le jeton ci-dessus est signé de manière cryptographique et peut donc être vérifié, fournissant la preuve qu'il n'a pas été falsifié. De plus, les JWT sont codés avec une variété d'informations supplémentaires.

Examinons l'anatomie d'un JWT pour mieux comprendre comment nous en tirons toute cette bonté. Vous avez peut-être remarqué qu'il existe trois sections distinctes séparées par des points (.):

Entête

eyJhbGciOiJIUzI1NiJ9

Charge utile

eyJqdGkiOiJlNjc4ZjIzMzQ3ZTM0MTBkYjdlNjg3Njc4MjNiMmQ3MCIsImlhdC I6MTQ2NjYzMzMxNywibmJmIjoxNDY2NjMzMzYT3joxNDY2NjMzMzYT2JEHJEH

Signature

rgx_o8VQGuDa2AqCHSgVOD5G68Ld_YYM7N7THmvLIKc

Chaque section est encodée en URLbase64. Cela garantit qu'il peut être utilisé en toute sécurité dans une URL (pour plus d'informations à ce sujet plus tard). Examinons de plus près chaque section individuellement.

2.1. L'en-tête

Si vous voulez décoder l'en-tête en base64, vous obtiendrez la chaîne JSON suivante:

{"alg":"HS256"}

Cela montre que le JWT a été signé avecHMAC en utilisantSHA-256.

2.2. La charge utile

Si vous décodez la charge, vous obtenez la chaîne JSON suivante (formatée pour la clarté):

{
  "jti": "e678f23347e3410db7e68767823b2d70",
  "iat": 1466633317,
  "nbf": 1466633317,
  "exp": 1466636917
}

Comme vous pouvez le constater, la charge utile contient un certain nombre de clés avec des valeurs. Ces clés sont appelées «revendications» et leJWT specification en a sept spécifiées comme des revendications «enregistrées». Elles sont:

iss

Émetteur

sub

Matière

aud

Public

exp

Expiration

nbf

Pas avant

iat

Émis à

jti

ID JWT

Lors de la construction d'un JWT, vous pouvez définir toutes les revendications personnalisées de votre choix. La liste ci-dessus représente simplement les revendications réservées à la fois dans la clé utilisée et le type attendu. Notre CSRF a un ID JWT, une heure «Émis à», une heure «Pas avant» et une heure d'expiration. Le délai d’expiration est exactement une minute après le délai émis.

2.3. La signature

Enfin, la section de signature est créée en prenant l’en-tête et les données utiles (avec le. entre les deux) et en le passant à travers l'algorithme spécifié (HMAC utilisant SHA-256, dans ce cas) avec un secret connu. Notez que le secret estalways un tableau d'octets, et doit être d'une longueur qui a du sens pour l'algorithme utilisé. Ci-dessous, j'utilise une chaîne encodée en base64 aléatoire (pour la lisibilité) qui est convertie en un tableau d'octets.

Cela ressemble à ceci dans le pseudo-code:

computeHMACSHA256(
    header + "." + payload,
    base64DecodeToByteArray("4pE8z3PBoHjnV1AhvGk+e8h2p+ShZpOnpr8cwHmMh1w=")
)

Tant que vous connaissez le secret, vous pouvez générer la signature vous-même et comparer votre résultat à la section de signature du JWT pour vérifier qu'il n'a pas été falsifié. Techniquement, un JWT qui a été signé de manière cryptographique est appelé unJWS. Les JWT peuvent également être chiffrés et seraient alors appelésJWE. (En pratique réelle, le terme JWT est utilisé pour décrire les JWE et les JWS.)

Cela nous ramène aux avantages d'utiliser un JWT comme jeton CSRF. Nous pouvons vérifier la signature et utiliser les informations codées dans le JWT pour confirmer sa validité. Ainsi, non seulement la représentation sous forme de chaîne du JWT doit correspondre à ce qui est stocké côté serveur, mais nous pouvons nous assurer qu’elle n’a pas expiré simplement en inspectant la revendicationexp. Cela évite au serveur de conserver un état supplémentaire.

Eh bien, nous avons couvert beaucoup de terrain ici. Plongeons dans un peu de code!

3. Configurer le didacticiel JJWT

JJWT (https://github.com/jwtk/jjwt) est une bibliothèque Java fournissant la création et la vérification de jetons Web JSON de bout en bout. Toujours libre et à source ouverte (licence Apache, version 2.0), il a été conçu avec une interface dédiée au constructeur masquant la majeure partie de sa complexité.

Les principales opérations d’utilisation de JJWT consistent à créer et à analyser des JWT. Nous examinerons ces opérations ensuite, puis aborderons certaines fonctionnalités étendues du JJWT, et enfin, nous verrons les JWT en action en tant que jetons CSRF dans une application Spring Security, Spring Boot.

Le code illustré dans les sections suivantes peut être trouvéhere. Remarque: Le projet utilise Spring Boot depuis le début car il est facile d'interagir avec l'API qu'il expose.

Pour construire le projet, exécutez ce qui suit:

git clone https://github.com/eugenp/tutorials.git
cd tutorials/jjwt
mvn clean install

L'un des avantages de Spring Boot est la facilité avec laquelle il est possible de lancer une application. Pour exécuter l'application JJWT Fun, procédez comme suit:

java -jar target/*.jar

Il existe dix points de terminaison exposés dans cet exemple d'application (j'utilise httpie pour interagir avec l'application. Il peut être trouvéhere.)

http localhost:8080
Available commands (assumes httpie - https://github.com/jkbrzt/httpie):

  http http://localhost:8080/
    This usage message

  http http://localhost:8080/static-builder
    build JWT from hardcoded claims

  http POST http://localhost:8080/dynamic-builder-general claim-1=value-1 ... [claim-n=value-n]
    build JWT from passed in claims (using general claims map)

  http POST http://localhost:8080/dynamic-builder-specific claim-1=value-1 ... [claim-n=value-n]
    build JWT from passed in claims (using specific claims methods)

  http POST http://localhost:8080/dynamic-builder-compress claim-1=value-1 ... [claim-n=value-n]
    build DEFLATE compressed JWT from passed in claims

  http http://localhost:8080/parser?jwt=
    Parse passed in JWT

  http http://localhost:8080/parser-enforce?jwt=
    Parse passed in JWT enforcing the 'iss' registered claim and the 'hasMotorcycle' custom claim

  http http://localhost:8080/get-secrets
    Show the signing keys currently in use.

  http http://localhost:8080/refresh-secrets
    Generate new signing keys and show them.

  http POST http://localhost:8080/set-secrets
    HS256=base64-encoded-value HS384=base64-encoded-value HS512=base64-encoded-value
    Explicitly set secrets to use in the application.

Dans les sections suivantes, nous examinerons chacun de ces points de terminaison et le code JJWT contenu dans les gestionnaires.

4. Création de JWT avec JJWT

En raison desfluent interface de JJWT, la création du JWT est essentiellement un processus en trois étapes:

  1. Définition des revendications internes du jeton, telles que Emetteur, Objet, Expiration et ID.

  2. La signature cryptographique du JWT (en faisant un JWS).

  3. Le compactage du JWT en une chaîne URL sécurisée, selon les règlesJWT Compact Serialization.

Le fichier JWT final sera une chaîne en trois parties codée en base64, signée avec l'algorithme de signature spécifié et utilisant la clé fournie. Après ce moment, le jeton est prêt à être partagé avec une autre partie.

Voici un exemple du JJWT en action:

String jws = Jwts.builder()
  .setIssuer("Stormpath")
  .setSubject("msilverman")
  .claim("name", "Micah Silverman")
  .claim("scope", "admins")
  // Fri Jun 24 2016 15:33:42 GMT-0400 (EDT)
  .setIssuedAt(Date.from(Instant.ofEpochSecond(1466796822L)))
  // Sat Jun 24 2116 15:33:42 GMT-0400 (EDT)
  .setExpiration(Date.from(Instant.ofEpochSecond(4622470422L)))
  .signWith(
    SignatureAlgorithm.HS256,
    TextCodec.BASE64.decode("Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=")
  )
  .compact();

Ceci est très similaire au code de la méthodeStaticJWTController.fixedBuilder du projet de code.

À ce stade, cela vaut la peine de parler de quelques anti-modèles liés aux JWT et à la signature. Si vous avez déjà vu des exemples JWT auparavant, vous avez probablement rencontré l'un de ces scénarios anti-pattern de signature:

  1. .signWith(
        SignatureAlgorithm.HS256,
       "secret".getBytes("UTF-8")
    )
  2. .signWith(
        SignatureAlgorithm.HS256,
        "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=".getBytes("UTF-8")
    )
  3. .signWith(
        SignatureAlgorithm.HS512,
        TextCodec.BASE64.decode("Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=")
    )

Tout algorithme de signature de typeHS prend un tableau d'octets. Il est pratique pour les humains de lire pour prendre une chaîne et la convertir en tableau d'octets.

L'anti-motif 1 ci-dessus le démontre. Ceci est problématique car le secret est affaibli par sa courte durée et ce n’est pas un tableau d’octets dans sa forme native. Donc, pour le garder lisible, nous pouvons encoder en base64 le tableau d'octets.

Cependant, anti-pattern 2 ci-dessus prend la chaîne codée en base64 et la convertit directement en tableau d'octets. Ce qui devrait être fait est de décoder la chaîne base64 dans le tableau d'octets d'origine.

Le numéro 3 ci-dessus le démontre. Alors, pourquoi est-ce aussi un anti-modèle? C’est une raison subtile dans ce cas. Notez que l'algorithme de signature est HS512. Le tableau d'octets n'est pas la longueur maximale queHS512 peut prendre en charge, ce qui en fait un secret plus faible que ce qui est possible pour cet algorithme.

L'exemple de code comprend une classe appeléeSecretService qui garantit que des secrets de force appropriée sont utilisés pour l'algorithme donné. Au démarrage de l'application, un nouvel ensemble de secrets est créé pour chacun des algorithmes HS. Il existe des points de terminaison pour actualiser les secrets et définir explicitement les secrets.

Si le projet fonctionne comme décrit ci-dessus, exécutez la procédure suivante pour que les exemples JWT ci-dessous correspondent aux réponses de votre projet.

http POST localhost:8080/set-secrets \
  HS256="Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=" \
  HS384="VW96zL+tYlrJLNCQ0j6QPTp+d1q75n/Wa8LVvpWyG8pPZOP6AA5X7XOIlI90sDwx" \
  HS512="cd+Pr1js+w2qfT2BoCD+tPcYp9LbjpmhSMEJqUob1mcxZ7+Wmik4AYdjX+DlDjmE4yporzQ9tm7v3z/j+QbdYg=="

Maintenant, vous pouvez atteindre le point de terminaison/static-builder:

http http://localhost:8080/static-builder

Cela produit un JWT qui ressemble à ceci:

eyJhbGciOiJIUzI1NiJ9.
eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIiwiaWF0IjoxNDY2Nzk2ODIyLCJleHAiOjQ2MjI0NzA0MjJ9.
kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ

Maintenant, appuyez sur:

http http://localhost:8080/parser?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIiwiaWF0IjoxNDY2Nzk2ODIyLCJleHAiOjQ2MjI0NzA0MjJ9.kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ

La réponse contient toutes les revendications que nous avons incluses lors de la création du JWT.

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
...
{
    "jws": {
        "body": {
            "exp": 4622470422,
            "iat": 1466796822,
            "iss": "Stormpath",
            "name": "Micah Silverman",
            "scope": "admins",
            "sub": "msilverman"
        },
        "header": {
            "alg": "HS256"
        },
        "signature": "kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ"
    },
    "status": "SUCCESS"
}

Il s’agit de l’opération d’analyse que nous aborderons dans la section suivante.

Maintenant, abordons un point de terminaison qui prend les revendications comme paramètres et qui construira un JWT personnalisé pour nous.

http -v POST localhost:8080/dynamic-builder-general iss=Stormpath sub=msilverman hasMotorcycle:=true

Note: il existe une différence subtile entre la revendicationhasMotorcycle et les autres revendications. httpie suppose que les paramètres JSON sont des chaînes par défaut. Pour soumettre du JSON brut à l'aide de httpie, vous utilisez le formulaire:= plutôt que=. Sans cela, il soumettrait“hasMotorcycle”: “true”, ce qui n'est pas ce que nous voulons.

Voici la sortie:

POST /dynamic-builder-general HTTP/1.1
Accept: application/json
...
{
    "hasMotorcycle": true,
    "iss": "Stormpath",
    "sub": "msilverman"
}

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
...
{
    "jwt":
      "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwiaGFzTW90b3JjeWNsZSI6dHJ1ZX0.OnyDs-zoL3-rw1GaSl_KzZzHK9GoiNocu-YwZ_nQNZU",
    "status": "SUCCESS"
}

Jetons un coup d'œil au code qui soutient ce point de terminaison:

@RequestMapping(value = "/dynamic-builder-general", method = POST)
public JwtResponse dynamicBuilderGeneric(@RequestBody Map claims)
  throws UnsupportedEncodingException {
    String jws =  Jwts.builder()
        .setClaims(claims)
        .signWith(
            SignatureAlgorithm.HS256,
            secretService.getHS256SecretBytes()
        )
        .compact();
    return new JwtResponse(jws);
}

La ligne 2 garantit que le JSON entrant est automatiquement converti en une mappe Java , ce qui est très pratique pour JJWT car la méthode de la ligne 5 prend simplement cette mappe et définit toutes les revendications en même temps.

Aussi sommaire que soit ce code, nous avons besoin de quelque chose de plus spécifique pour garantir la validité des revendications transmises. L'utilisation de la méthode.setClaims(Map<String, Object> claims) est pratique lorsque vous savez déjà que les revendications représentées dans la carte sont valides. C'est là que la sécurité de type de Java entre dans la bibliothèque JJWT.

Pour chacune des revendications enregistrées définies dans la spécification JWT, il existe une méthode Java correspondante dans le JJWT qui prend le type correct.

Examinons un autre point de terminaison dans notre exemple et voyons ce qui se passe:

http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath sub:=5 hasMotorcycle:=true

Notez que nous avons transmis un entier, 5, pour la revendication "sous". Voici la sortie:

POST /dynamic-builder-specific HTTP/1.1
Accept: application/json
...
{
    "hasMotorcycle": true,
    "iss": "Stormpath",
    "sub": 5
}

HTTP/1.1 400 Bad Request
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
    "exceptionType": "java.lang.ClassCastException",
    "message": "java.lang.Integer cannot be cast to java.lang.String",
    "status": "ERROR"
}

Maintenant, nous recevons une réponse d'erreur car le code applique le type des revendications enregistrées. Dans ce cas,sub doit être une chaîne. Voici le code qui soutient ce point de terminaison:

@RequestMapping(value = "/dynamic-builder-specific", method = POST)
public JwtResponse dynamicBuilderSpecific(@RequestBody Map claims)
  throws UnsupportedEncodingException {
    JwtBuilder builder = Jwts.builder();

    claims.forEach((key, value) -> {
        switch (key) {
            case "iss":
                builder.setIssuer((String) value);
                break;
            case "sub":
                builder.setSubject((String) value);
                break;
            case "aud":
                builder.setAudience((String) value);
                break;
            case "exp":
                builder.setExpiration(Date.from(
                    Instant.ofEpochSecond(Long.parseLong(value.toString()))
                ));
                break;
            case "nbf":
                builder.setNotBefore(Date.from(
                    Instant.ofEpochSecond(Long.parseLong(value.toString()))
                ));
                break;
            case "iat":
                builder.setIssuedAt(Date.from(
                    Instant.ofEpochSecond(Long.parseLong(value.toString()))
                ));
                break;
            case "jti":
                builder.setId((String) value);
                break;
            default:
                builder.claim(key, value);
        }
    });

    builder.signWith(SignatureAlgorithm.HS256, secretService.getHS256SecretBytes());

    return new JwtResponse(builder.compact());
}

Tout comme avant, la méthode accepte unMap<String, Object> de revendications comme paramètre. Cependant, cette fois, nous appelons la méthode spécifique pour chacune des revendications enregistrées qui appliquent le type.

Un raffinement consiste à rendre le message d'erreur plus spécifique. À l'heure actuelle, nous savons seulement que l'une de nos revendications n'est pas du type correct. Nous ne savons pas quelle réclamation était erronée ni ce qu'elle devrait être. Voici une méthode qui nous donnera un message d'erreur plus spécifique. Il traite également d'un bogue dans le code actuel.

private void ensureType(String registeredClaim, Object value, Class expectedType) {
    boolean isCorrectType =
        expectedType.isInstance(value) ||
        expectedType == Long.class && value instanceof Integer;

    if (!isCorrectType) {
        String msg = "Expected type: " + expectedType.getCanonicalName() +
            " for registered claim: '" + registeredClaim + "', but got value: " +
            value + " of type: " + value.getClass().getCanonicalName();
        throw new JwtException(msg);
    }
}

La ligne 3 vérifie que la valeur transmise est du type attendu. Sinon, unJwtException est renvoyé avec l'erreur spécifique. Jetons un coup d'œil à cela en action en faisant le même appel que nous l'avons fait précédemment:

http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath sub:=5 hasMotorcycle:=true
POST /dynamic-builder-specific HTTP/1.1
Accept: application/json
...
User-Agent: HTTPie/0.9.3

{
    "hasMotorcycle": true,
    "iss": "Stormpath",
    "sub": 5
}

HTTP/1.1 400 Bad Request
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
    "exceptionType": "io.jsonwebtoken.JwtException",
    "message":
      "Expected type: java.lang.String for registered claim: 'sub', but got value: 5 of type: java.lang.Integer",
    "status": "ERROR"
}

Maintenant, nous avons un message d'erreur très spécifique nous indiquant que la revendicationsub est celle qui est erronée.

Revenons à ce bogue dans notre code. Le problème n'a rien à voir avec la bibliothèque JJWT. Le problème est que le mappeur d'objets JSON vers Java intégré à Spring Boot est trop intelligent pour notre bien.

S'il existe une méthode qui accepte un objet Java, le mappeur JSON convertira automatiquement un nombre transmis inférieur ou égal à 2 147 483 647 en un JavaInteger. De même, il convertira automatiquement un nombre passé supérieur à 2 147 483 647 en un JavaLong. Pour les revendicationsiat,nbf etexp d'un JWT, nous voulons que notre test ensureType réussisse, que l'objet mappé soit un entier ou un long. C’est pourquoi nous avons la clause supplémentaire pour déterminer si la valeur transmise est du type correct:

 boolean isCorrectType =
     expectedType.isInstance(value) ||
     expectedType == Long.class && value instanceof Integer;

Si nous attendons un Long, mais que la valeur est une instance de Integer, nous disons toujours que c'est le type correct. Avec une compréhension de ce qui se passe avec cette validation, nous pouvons maintenant l'intégrer dans notre méthodedynamicBuilderSpecific:

@RequestMapping(value = "/dynamic-builder-specific", method = POST)
public JwtResponse dynamicBuilderSpecific(@RequestBody Map claims)
  throws UnsupportedEncodingException {
    JwtBuilder builder = Jwts.builder();

    claims.forEach((key, value) -> {
        switch (key) {
            case "iss":
                ensureType(key, value, String.class);
                builder.setIssuer((String) value);
                break;
            case "sub":
                ensureType(key, value, String.class);
                builder.setSubject((String) value);
                break;
            case "aud":
                ensureType(key, value, String.class);
                builder.setAudience((String) value);
                break;
            case "exp":
                ensureType(key, value, Long.class);
                builder.setExpiration(Date.from(
                    Instant.ofEpochSecond(Long.parseLong(value.toString()))
                ));
                break;
            case "nbf":
                ensureType(key, value, Long.class);
                builder.setNotBefore(Date.from(
                    Instant.ofEpochSecond(Long.parseLong(value.toString()))
                ));
                break;
            case "iat":
                ensureType(key, value, Long.class);
                builder.setIssuedAt(Date.from(
                    Instant.ofEpochSecond(Long.parseLong(value.toString()))
                ));
                break;
            case "jti":
                ensureType(key, value, String.class);
                builder.setId((String) value);
                break;
            default:
                builder.claim(key, value);
        }
    });

    builder.signWith(SignatureAlgorithm.HS256, secretService.getHS256SecretBytes());

    return new JwtResponse(builder.compact());
}

Note: dans tous les exemples de code de cette section, les JWT sont signés avec le HMAC à l'aide de l'algorithme SHA-256. C'est pour garder les exemples simples. La bibliothèque JJWT prend en charge 12 algorithmes de signature différents dont vous pouvez tirer parti dans votre propre code.

5. Analyse des JWT avec JJWT

Nous avons vu précédemment que notre exemple de code comporte un point de terminaison pour l'analyse d'un fichier JWT. Frapper ce point final:

http http://localhost:8080/parser?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIiwiaWF0IjoxNDY2Nzk2ODIyLCJleHAiOjQ2MjI0NzA0MjJ9.kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ

produit cette réponse:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
...
{
    "claims": {
        "body": {
            "exp": 4622470422,
            "iat": 1466796822,
            "iss": "Stormpath",
            "name": "Micah Silverman",
            "scope": "admins",
            "sub": "msilverman"
        },
        "header": {
            "alg": "HS256"
        },
        "signature": "kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ"
    },
    "status": "SUCCESS"
}

La méthodeparser de la classeStaticJWTController ressemble à ceci:

@RequestMapping(value = "/parser", method = GET)
public JwtResponse parser(@RequestParam String jwt) throws UnsupportedEncodingException {
    Jws jws = Jwts.parser()
        .setSigningKeyResolver(secretService.getSigningKeyResolver())
        .parseClaimsJws(jwt);

    return new JwtResponse(jws);
}

La ligne 4 indique que nous nous attendons à ce que la chaîne entrante soit un JWT signé (un JWS). Et nous utilisons le même secret que celui utilisé pour signer le JWT lors de son analyse. La ligne 5 analyse les revendications du JWT. En interne, il vérifie la signature et il lève une exception si la signature est invalide.

Notez que dans ce cas, nous passons unSigningKeyResolver plutôt qu'une clé elle-même. C'est l'un des aspects les plus puissants de JJWT. L'en-tête de JWT indique l'algorithme utilisé pour le signer. Cependant, nous devons vérifier le JWT avant de lui faire confiance. Cela semblerait être un piège 22. Regardons la méthodeSecretService.getSigningKeyResolver:

private SigningKeyResolver signingKeyResolver = new SigningKeyResolverAdapter() {
    @Override
    public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
        return TextCodec.BASE64.decode(secrets.get(header.getAlgorithm()));
    }
};

En utilisant l'accès auxJwsHeader, je peux inspecter l'algorithme et renvoyer le tableau d'octets approprié pour le secret qui a été utilisé pour signer le JWT. Maintenant, JJWT vérifiera qu'il n'a pas été falsifié en utilisant ce tableau d'octets en tant que clé.

Si je supprime le dernier caractère du JWT passé (qui fait partie de la signature), voici la réponse:

HTTP/1.1 400 Bad Request
Connection: close
Content-Type: application/json;charset=UTF-8
Date: Mon, 27 Jun 2016 13:19:08 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked

{
    "exceptionType": "io.jsonwebtoken.SignatureException",
    "message":
      "JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.",
    "status": "ERROR"
}

6. Les JWT en pratique: jetons Spring Security CSRF

Bien que l’objet de ce billet ne soit pas Spring Security, nous allons en parler un peu plus en détail pour présenter l’utilisation concrète de la bibliothèque JJWT.

Cross Site Request Forgery est une vulnérabilité de sécurité par laquelle un site Web malveillant vous incite à soumettre des demandes à un site Web avec lequel vous avez établi une relation de confiance. L'un des remèdes les plus courants consiste à implémenter unsynchronizer token pattern. Cette approche insère un jeton dans le formulaire Web et le serveur d'applications vérifie le jeton entrant par rapport à son référentiel pour confirmer qu'il est correct. Si le jeton est manquant ou invalide, le serveur répondra avec une erreur.

Spring Security intègre le modèle de jeton de synchronisation. Mieux encore, si vous utilisez lesSpring Boot and Thymeleaf templates, le jeton de synchronisation est automatiquement inséré pour vous.

Par défaut, le jeton utilisé par Spring Security est un jeton «muet». Ce n’est qu’une série de lettres et de chiffres. Cette approche est très bien et ça marche. Dans cette section, nous améliorons les fonctionnalités de base en utilisant les JWT comme jeton. En plus de vérifier que le jeton soumis est celui attendu, nous validons le JWT pour prouver de plus que le jeton n'a pas été falsifié et pour s'assurer qu'il n'est pas expiré.

Pour commencer, nous allons configurer Spring Security à l'aide de la configuration Java. Par défaut, tous les chemins nécessitent une authentification et tous les noeuds finaux POST nécessitent des jetons CSRF. Nous allons assouplir cela un peu pour que ce que nous avons construit jusqu'à présent fonctionne toujours.

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private String[] ignoreCsrfAntMatchers = {
        "/dynamic-builder-compress",
        "/dynamic-builder-general",
        "/dynamic-builder-specific",
        "/set-secrets"
    };

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
                .ignoringAntMatchers(ignoreCsrfAntMatchers)
            .and().authorizeRequests()
                .antMatchers("/**")
                .permitAll();
    }
}

Nous faisons deux choses ici. Tout d'abord, nous disons que les jetons CSRF sontnotrequis lors de la publication sur nos points de terminaison d'API REST (ligne 15). Deuxièmement, nous disons qu'un accès non authentifié devrait être autorisé pour tous les chemins (lignes 17 à 18).

Confirmons que Spring Security fonctionne comme prévu. Lancez l'application et cliquez sur cette URL dans votre navigateur:

http://localhost:8080/jwt-csrf-form

Voici le modèle Thymeleaf pour cette vue:



    
        
    
    
        

Il s'agit d'un formulaire très basique qui sera posté sur le même point de terminaison lors de sa soumission. Notez qu'il n'y a aucune référence explicite aux jetons CSRF dans le formulaire. Si vous affichez la source, vous verrez quelque chose comme:

C'est toute la confirmation dont vous avez besoin pour savoir que Spring Security fonctionne et que les modèles Thymeleaf insèrent automatiquement le jeton CSRF.

Pour faire de la valeur un JWT, nous allons activer unCsrfTokenRepository personnalisé. Voici comment notre configuration de Spring Security change:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    CsrfTokenRepository jwtCsrfTokenRepository;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
                .csrfTokenRepository(jwtCsrfTokenRepository)
                .ignoringAntMatchers(ignoreCsrfAntMatchers)
            .and().authorizeRequests()
                .antMatchers("/**")
                .permitAll();
    }
}

Pour ce faire, nous avons besoin d’une configuration exposant un bean renvoyant le référentiel de jetons personnalisé. Voici la configuration:

@Configuration
public class CSRFConfig {

    @Autowired
    SecretService secretService;

    @Bean
    @ConditionalOnMissingBean
    public CsrfTokenRepository jwtCsrfTokenRepository() {
        return new JWTCsrfTokenRepository(secretService.getHS256SecretBytes());
    }
}

Et voici notre référentiel personnalisé (les éléments importants):

public class JWTCsrfTokenRepository implements CsrfTokenRepository {

    private static final Logger log = LoggerFactory.getLogger(JWTCsrfTokenRepository.class);
    private byte[] secret;

    public JWTCsrfTokenRepository(byte[] secret) {
        this.secret = secret;
    }

    @Override
    public CsrfToken generateToken(HttpServletRequest request) {
        String id = UUID.randomUUID().toString().replace("-", "");

        Date now = new Date();
        Date exp = new Date(System.currentTimeMillis() + (1000*30)); // 30 seconds

        String token;
        try {
            token = Jwts.builder()
                .setId(id)
                .setIssuedAt(now)
                .setNotBefore(now)
                .setExpiration(exp)
                .signWith(SignatureAlgorithm.HS256, secret)
                .compact();
        } catch (UnsupportedEncodingException e) {
            log.error("Unable to create CSRf JWT: {}", e.getMessage(), e);
            token = id;
        }

        return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token);
    }

    @Override
    public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
        ...
    }

    @Override
    public CsrfToken loadToken(HttpServletRequest request) {
        ...
    }
}

La méthodegenerateToken crée un JWT qui expire 30 secondes après sa création. Avec cette plomberie en place, nous pouvons relancer l'application et regarder la source de/jwt-csrf-form.

Maintenant, le champ caché ressemble à ceci:

Huzzah! Notre jeton CSRF est maintenant un JWT. Ce n’était pas trop difficile.

Cependant, ce n'est que la moitié du casse-tête. Par défaut, Spring Security enregistre simplement le jeton CSRF et confirme que le jeton soumis dans un formulaire Web correspond à celui qui a été enregistré. Nous voulons étendre la fonctionnalité pour valider le JWT et nous assurer qu'il n'est pas expiré. Pour ce faire, nous ajouterons un filtre. Voici à quoi ressemble notre configuration Spring Security:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    ...

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .addFilterAfter(new JwtCsrfValidatorFilter(), CsrfFilter.class)
            .csrf()
                .csrfTokenRepository(jwtCsrfTokenRepository)
                .ignoringAntMatchers(ignoreCsrfAntMatchers)
            .and().authorizeRequests()
                .antMatchers("/**")
                .permitAll();
    }

    ...
}

À la ligne 9, nous avons ajouté un filtre et nous le plaçons dans la chaîne de filtres après lesCsrfFilter par défaut. Ainsi, au moment où notre filtre est atteint, le jeton JWT (dans son ensemble) aura déjà été confirmé comme étant la valeur correcte enregistrée par Spring Security.

Voici lesJwtCsrfValidatorFilter (c'est privé car il s'agit d'une classe interne de notre configuration Spring Security):

private class JwtCsrfValidatorFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(
      HttpServletRequest request,
      HttpServletResponse response,
      FilterChain filterChain) throws ServletException, IOException {
        // NOTE: A real implementation should have a nonce cache so the token cannot be reused
        CsrfToken token = (CsrfToken) request.getAttribute("_csrf");

        if (
            // only care if it's a POST
            "POST".equals(request.getMethod()) &&
            // ignore if the request path is in our list
            Arrays.binarySearch(ignoreCsrfAntMatchers, request.getServletPath()) < 0 &&
            // make sure we have a token
            token != null
        ) {
            // CsrfFilter already made sure the token matched.
            // Here, we'll make sure it's not expired
            try {
                Jwts.parser()
                    .setSigningKey(secret.getBytes("UTF-8"))
                    .parseClaimsJws(token.getToken());
            } catch (JwtException e) {
                // most likely an ExpiredJwtException, but this will handle any
                request.setAttribute("exception", e);
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                RequestDispatcher dispatcher = request.getRequestDispatcher("expired-jwt");
                dispatcher.forward(request, response);
            }
        }

        filterChain.doFilter(request, response);
    }
}

Regardez la ligne 23 sur. Nous analysons le JWT comme auparavant. Dans ce cas, si une exception est levée, la demande est transmise au modèleexpired-jwt. Si le JWT est validé, le traitement continue normalement.

Cela ferme la boucle en remplaçant le comportement du jeton Spring Security CSRF par défaut avec un référentiel de jetons JWT et un validateur.

Si vous lancez l'application, accédez à/jwt-csrf-form, attendez un peu plus de 30 secondes et cliquez sur le bouton, vous verrez quelque chose comme ceci:

jwt_expired

7. Fonctionnalités étendues de JJWT

Nous terminerons notre parcours JJWT par un mot sur certaines des fonctionnalités qui vont au-delà des spécifications.

7.1. Appliquer les réclamations

Dans le cadre du processus d'analyse, JJWT vous permet de spécifier les revendications requises et les valeurs que ces revendications doivent avoir. Ceci est très pratique si certaines informations dans vos JWT doivent être présentes pour que vous puissiez les considérer comme valides. Cela évite beaucoup de logique de branchement pour valider manuellement les revendications. Voici la méthode qui sert le point de terminaison/parser-enforce de notre exemple de projet.

@RequestMapping(value = "/parser-enforce", method = GET)
public JwtResponse parserEnforce(@RequestParam String jwt)
  throws UnsupportedEncodingException {
    Jws jws = Jwts.parser()
        .requireIssuer("Stormpath")
        .require("hasMotorcycle", true)
        .setSigningKeyResolver(secretService.getSigningKeyResolver())
        .parseClaimsJws(jwt);

    return new JwtResponse(jws);
}

Les lignes 5 et 6 indiquent la syntaxe des revendications enregistrées et personnalisées. Dans cet exemple, le JWT sera considéré comme non valide si la revendication iss n'est pas présente ou n'a pas la valeur: Stormpath. Il sera également invalide si la revendication personnalisée hasMotorcycle n’est pas présente ou n’a pas la valeur: true.

Créons d'abord un JWT qui suit le chemin heureux:

http -v POST localhost:8080/dynamic-builder-specific \
  iss=Stormpath hasMotorcycle:=true sub=msilverman
POST /dynamic-builder-specific HTTP/1.1
Accept: application/json
...
{
    "hasMotorcycle": true,
    "iss": "Stormpath",
    "sub": "msilverman"
}

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
...
{
    "jwt":
      "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIn0.qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0",
    "status": "SUCCESS"
}

Maintenant, validons ce JWT:

http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIn0.qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0
GET /parser-enforce?jwt=http
  -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIn0.qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0 HTTP/1.1
Accept: */*
...
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
...
{
    "jws": {
        "body": {
            "hasMotorcycle": true,
            "iss": "Stormpath",
            "sub": "msilverman"
        },
        "header": {
            "alg": "HS256"
        },
        "signature": "qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0"
    },
    "status": "SUCCESS"
}

Jusqu'ici tout va bien. Maintenant, cette fois, laissons la hasMotorcycle de côté:

http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath sub=msilverman

Cette fois, si nous essayons de valider le JWT:

http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIn0.YMONlFM1tNgttUYukDRsi9gKIocxdGAOLaJBymaQAWc

on a:

GET /parser-enforce?jwt=http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIn0.YMONlFM1tNgttUYukDRsi9gKIocxdGAOLaJBymaQAWc HTTP/1.1
Accept: */*
...
HTTP/1.1 400 Bad Request
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
    "exceptionType": "io.jsonwebtoken.MissingClaimException",
    "message":
      "Expected hasMotorcycle claim to be: true, but was not present in the JWT claims.",
    "status": "ERROR"
}

Cela indique que notre revendication hasMotorcycle était attendue mais était manquante.

Faisons un autre exemple:

http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath hasMotorcycle:=false sub=msilverman

Cette fois, la revendication requise est présente, mais sa valeur est incorrecte. Voyons le résultat de:

http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjpmYWxzZSwic3ViIjoibXNpbHZlcm1hbiJ9.8LBq2f0eINB34AzhVEgsln_KDo-IyeM8kc-dTzSCr0c
GET /parser-enforce?jwt=http
  -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjpmYWxzZSwic3ViIjoibXNpbHZlcm1hbiJ9.8LBq2f0eINB34AzhVEgsln_KDo-IyeM8kc-dTzSCr0c HTTP/1.1
Accept: */*
...
HTTP/1.1 400 Bad Request
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
    "exceptionType": "io.jsonwebtoken.IncorrectClaimException",
    "message": "Expected hasMotorcycle claim to be: true, but was: false.",
    "status": "ERROR"
}

Cela indique que notre revendication hasMotorcycle était présente, mais que sa valeur était inattendue.

MissingClaimException etIncorrectClaimException sont vos amis lors de l'application des revendications dans vos JWT et une fonctionnalité que seule la bibliothèque JJWT possède.

7.2. Compression JWT

Si vous avez beaucoup de revendications sur un JWT, celui-ci peut devenir trop gros, à tel point qu'il ne rentre peut-être pas dans une URL GET dans certains navigateurs.

Créons un grand JWT:

http -v POST localhost:8080/dynamic-builder-specific \
  iss=Stormpath hasMotorcycle:=true sub=msilverman the=quick brown=fox jumped=over lazy=dog \
  somewhere=over rainbow=way up=high and=the dreams=you dreamed=of

Voici le JWT qui produit:

eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIiwidGhlIjoicXVpY2siLCJicm93biI6ImZveCIsImp1bXBlZCI6Im92ZXIiLCJsYXp5IjoiZG9nIiwic29tZXdoZXJlIjoib3ZlciIsInJhaW5ib3ciOiJ3YXkiLCJ1cCI6ImhpZ2giLCJhbmQiOiJ0aGUiLCJkcmVhbXMiOiJ5b3UiLCJkcmVhbWVkIjoib2YifQ.AHNJxSTiDw_bWNXcuh-LtPLvSjJqwDvOOUcmkk7CyZA

Cette ventouse est grosse! Maintenant, abordons un point de terminaison légèrement différent avec les mêmes revendications:

http -v POST localhost:8080/dynamic-builder-compress \
  iss=Stormpath hasMotorcycle:=true sub=msilverman the=quick brown=fox jumped=over lazy=dog \
  somewhere=over rainbow=way up=high and=the dreams=you dreamed=of

Cette fois, nous obtenons:

eyJhbGciOiJIUzI1NiIsImNhbGciOiJERUYifQ.eNpEzkESwjAIBdC7sO4JegdXnoC2tIk2oZLEGB3v7s84jjse_AFe5FOikc5ZLRycHQ3kOJ0Untu8C43ZigyUyoRYSH6_iwWOyGWHKd2Kn6_QZFojvOoDupRwyAIq4vDOzwYtugFJg1QnJv-5sY-TVjQqN7gcKJ3f-j8c-6J-baDFhEN_uGn58XtnpfcHAAD__w.3_wc-2skFBbInk0YAQ96yGWwr8r1xVdbHn-uGPTFuFE

62 caractères plus court! Voici le code de la méthode utilisée pour générer le JWT:

@RequestMapping(value = "/dynamic-builder-compress", method = POST)
public JwtResponse dynamicBuildercompress(@RequestBody Map claims)
  throws UnsupportedEncodingException {
    String jws =  Jwts.builder()
        .setClaims(claims)
        .compressWith(CompressionCodecs.DEFLATE)
        .signWith(
            SignatureAlgorithm.HS256,
            secretService.getHS256SecretBytes()
        )
        .compact();
    return new JwtResponse(jws);
}

Remarquez sur la ligne 6, nous spécifions un algorithme de compression à utiliser. C'est tout ce qu'on peut en dire.

Qu'en est-il de l'analyse des JWT compressés? La bibliothèque JJWT détecte automatiquement la compression et utilise le même algorithme pour décompresser:

GET /parser?jwt=eyJhbGciOiJIUzI1NiIsImNhbGciOiJERUYifQ.eNpEzkESwjAIBdC7sO4JegdXnoC2tIk2oZLEGB3v7s84jjse_AFe5FOikc5ZLRycHQ3kOJ0Untu8C43ZigyUyoRYSH6_iwWOyGWHKd2Kn6_QZFojvOoDupRwyAIq4vDOzwYtugFJg1QnJv-5sY-TVjQqN7gcKJ3f-j8c-6J-baDFhEN_uGn58XtnpfcHAAD__w.3_wc-2skFBbInk0YAQ96yGWwr8r1xVdbHn-uGPTFuFE HTTP/1.1
Accept: */*
...
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
...
{
    "claims": {
        "body": {
            "and": "the",
            "brown": "fox",
            "dreamed": "of",
            "dreams": "you",
            "hasMotorcycle": true,
            "iss": "Stormpath",
            "jumped": "over",
            "lazy": "dog",
            "rainbow": "way",
            "somewhere": "over",
            "sub": "msilverman",
            "the": "quick",
            "up": "high"
        },
        "header": {
            "alg": "HS256",
            "calg": "DEF"
        },
        "signature": "3_wc-2skFBbInk0YAQ96yGWwr8r1xVdbHn-uGPTFuFE"
    },
    "status": "SUCCESS"
}

Notez la revendicationcalg dans l'en-tête. Ceci a été automatiquement encodé dans le JWT et fournit à l'analyseur un indice sur l'algorithme à utiliser pour la décompression.

Note
La spécification JWE prend en charge la compression. Dans une prochaine version de la bibliothèque JJWT, nous prendrons en charge JWE et les JWE compressés. Nous continuerons à prendre en charge la compression dans d'autres types de JWT, même si elle n'est pas spécifiée.

8. Token Tools pour les développeurs Java

Bien que l'objectif principal de cet article ne soit pas Spring Boot ou Spring Security, l'utilisation de ces deux technologies a facilité la démonstration de toutes les fonctionnalités présentées dans cet article. Vous devriez être en mesure de démarrer le serveur et de commencer à jouer avec les différents points de terminaison dont nous avons discuté. Il suffit de frapper:

http http://localhost:8080

Stormpath est également ravi d'apporter un certain nombre d'outils de développement open source à la communauté Java. Ceux-ci inclus:

8.1. JJWT (de quoi nous parlons)

JJWT est untool for developers to create and verify JWTs in Java facile à utiliser. Comme beaucoup de bibliothèques prises en charge par Stormpath, JJWT est entièrement gratuit et à source ouverte (licence Apache, version 2.0), afin que tout le monde puisse voir ce qu’il fait et comment il le fait. N'hésitez pas à signaler tout problème, à suggérer des améliorations et même à envoyer du code!

8.2. jsonwebtoken.io and java.jsonwebtoken.io

jsonwebtoken.io est un outil de développement que nous avons créé pour faciliter le décodage des JWT. Collez simplement un JWT existant dans le champ approprié pour décoder son en-tête, sa charge utile et sa signature. jsonwebtoken.io is powered by nJWT, the cleanest free and open source (Apache License, Version 2.0) JWT library for Node.js developers. Vous pouvez également voir le code généré pour une variété de langues sur ce site. Le site Web lui-même est open-source et peut être trouvéhere.

java.jsonwebtoken.io est spécifiquement pour la bibliothèque JJWT. Vous pouvez modifier les en-têtes et les données utiles dans la zone supérieure droite, voir le fichier JWT généré par JJWT dans la zone supérieure gauche et voir un exemple du code Java du générateur et de l'analyseur dans les zones inférieures. Le site Web lui-même est open source et peut être trouvéhere.

8.3. Inspecteur JWT

Le nouveau venu sur le bloc,JWT Inspector est une extension Chrome open source qui permet aux développeurs d'inspecter et de déboguer les JWT directement dans le navigateur. L'inspecteur JWT découvre les JWT sur votre site (cookies, stockage local / de session et en-têtes) et les rend facilement accessibles via votre barre de navigation et votre panneau DevTools.

9. JWT ce bas!

Les JWT ajoutent une certaine intelligence aux jetons ordinaires. La possibilité de signer et de vérifier de manière cryptographique, de définir des délais d’expiration et de coder d’autres informations dans des fichiers JWT ouvre la voie à une gestion de session véritablement sans état. Cela a un impact important sur la capacité de dimensionner les applications.

Chez Stormpath, nous utilisons des JWT pour les jetons OAuth2, les jetons CSRF et les assertions entre microservices, entre autres utilisations.

Une fois que vous commencez à utiliser les JWT, vous ne pourrez plus jamais revenir aux marques idiotes du passé. Avez-vous des questions? Frappez-moi à@afitnerd sur Twitter.