Java-Authentifizierung mit JSON-Web-Token (JWTs) aufladen

Sind Sie bereit, eine sichere Authentifizierung in Ihrer Java-Anwendung zu erstellen oder Probleme damit zu haben? Sie sind sich nicht sicher, welche Vorteile die Verwendung von Token (und insbesondere von JSON-Web-Token) bietet oder wie diese bereitgestellt werden sollen? Ich freue mich darauf, diese und weitere Fragen in diesem Tutorial für Sie zu beantworten!

Bevor wir uns mit JSON Web Tokens (JWTs) und der JJWT-Bibliothek (erstellt von Stormpaths CTO Les Hazlewood, die von einer Community von Mitwirkenden verwaltet wird) befassen, wollen wir einige Grundlagen behandeln.

1. Authentifizierung vs. Token-Authentifizierung

Der Satz von Protokollen, mit denen eine Anwendung die Benutzeridentität bestätigt, ist die Authentifizierung. Anwendungen haben traditionell ihre Identität durch Sitzungscookies beibehalten. Dieses Paradigma beruht auf der serverseitigen Speicherung von Sitzungs-IDs, wodurch Entwickler gezwungen werden, einen Sitzungsspeicher zu erstellen, der entweder eindeutig und serverspezifisch ist oder als vollständig separate Sitzungsspeicherebene implementiert wird.

Die Token-Authentifizierung wurde entwickelt, um Probleme zu lösen, die serverseitige Sitzungs-IDs nicht und nicht konnten. Genau wie bei der herkömmlichen Authentifizierung legen Benutzer überprüfbare Anmeldeinformationen vor, erhalten jedoch jetzt eine Reihe von Token anstelle einer Sitzungs-ID. Die anfänglichen Anmeldeinformationen können das Standardpaar aus Benutzername und Kennwort, API-Schlüssel oder sogar Token von einem anderen Dienst sein. (Die API-Schlüsselauthentifizierungsfunktion von Stormpath ist ein Beispiel dafür.)

1.1. Warum Token?

Die Verwendung von Token anstelle von Sitzungs-IDs kann ganz einfach die Serverlast senken, die Berechtigungsverwaltung optimieren und bessere Tools für die Unterstützung einer verteilten oder Cloud-basierten Infrastruktur bereitstellen. Im Fall von JWT wird dies hauptsächlich durch die Staatenlosigkeit dieser Arten von Token erreicht (mehr dazu weiter unten).

Tokens bieten eine Vielzahl von Anwendungen, darunter: CSRF-Schutzschemata (Cross Site Request Forgery), OAuth 2.0-Interaktionen, Sitzungs-IDs und (in Cookies) als Authentifizierungsdarstellungen. In den meisten Fällen geben Standards kein bestimmtes Format für Token an. Hier ist ein Beispiel für ein typisches Spring Security CSRF-Token in einem HTML-Formular:

Wenn Sie versuchen, dieses Formular ohne das richtige CSRF-Token zu veröffentlichen, erhalten Sie eine Fehlerantwort. Dies ist das Dienstprogramm von Token. Das obige Beispiel ist ein "dummes" Token. Dies bedeutet, dass es keine inhärente Bedeutung gibt, die aus dem Token selbst abgeleitet werden könnte. Hier machen JWTs auch einen großen Unterschied.

2. Was ist in einem JWT?

JWTs (ausgesprochen „Jots“) sind URL-sichere, codierte, kryptografisch signierte (manchmal verschlüsselte) Zeichenfolgen, die in einer Vielzahl von Anwendungen als Token verwendet werden können. Hier ist ein Beispiel für eine JWT, die als CSRF-Token verwendet wird:

In diesem Fall können Sie sehen, dass das Token viel länger ist als in unserem vorherigen Beispiel. Wie wir bereits gesehen haben, erhalten Sie eine Fehlerantwort, wenn das Formular ohne das Token gesendet wird.

Warum also JWT?

Das oben genannte Token ist kryptografisch signiert und kann daher überprüft werden, um nachzuweisen, dass es nicht manipuliert wurde. Außerdem werden JWTs mit einer Vielzahl zusätzlicher Informationen codiert.

Schauen wir uns die Anatomie eines Zeugen Jehovas an, um besser zu verstehen, wie wir all diese Güte aus ihm herausdrücken. Möglicherweise haben Sie bemerkt, dass es drei verschiedene Abschnitte gibt, die durch Punkte ( .) getrennt sind:

Header eyJhbGciOiJIUzI1NiJ9
Nutzlast eyJqdGkiOiJlNjc4ZjIzMzQ3ZTM0MTBkYjdlNjg3Njc4MjNiMmQ3MCIsImlhdC

I6MTQ2NjYzMzMxNywibmJmIjoxNDY2NjMzMzE3LCJleHAiOjE0NjY2MzY5MTd9

Unterschrift rgx_o8VQGuDa2AqCHSgVOD5G68Ld_YYM7N7THmvLIKc

Jeder Abschnitt ist base64-URL-codiert. Dies stellt sicher, dass es sicher in einer URL verwendet werden kann (dazu später mehr). Schauen wir uns jeden Abschnitt einzeln genauer an.

2.1. Der Header

Wenn Sie den Header mit base64 dekodieren, erhalten Sie die folgende JSON-Zeichenfolge:

{"alg":"HS256"}

Dies zeigt, dass der JWT mit SHAC-256 mit HMAC signiert wurde.

2.2. Die Nutzlast

Wenn Sie die Nutzdaten dekodieren, erhalten Sie die folgende JSON-Zeichenfolge (aus Gründen der Übersichtlichkeit formatiert):

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

Wie Sie sehen können, gibt es innerhalb der Nutzlast eine Reihe von Schlüsseln mit Werten. Diese Schlüssel werden als "Ansprüche" bezeichnet, und in der JWT-Spezifikation sind sieben davon als "registrierte" Ansprüche angegeben. Sie sind:

iss Aussteller
sub Gegenstand
aud Publikum
exp Ablauf
nbf Nicht bevor
iat Ausgestellt bei
jti JWT ID

Beim Erstellen eines JWT können Sie beliebige benutzerdefinierte Ansprüche geltend machen. Die obige Liste stellt einfach die Ansprüche dar, die sowohl im verwendeten Schlüssel als auch im erwarteten Typ reserviert sind. Unsere CSRF hat eine JWT-ID, eine "Ausgestellt um" -Zeit, eine "Nicht vorher" -Zeit und eine Ablaufzeit. Die Ablaufzeit liegt genau eine Minute nach dem angegebenen Zeitpunkt.

2.3. Die Unterschrift

Schließlich wird der Signaturabschnitt erstellt, indem der Header und die Nutzdaten zusammen (mit dem dazwischen liegenden) zusammen mit einem bekannten Geheimnis durch den angegebenen Algorithmus (in diesem Fall HMAC mit SHA-256) geleitet werden. Beachten Sie, dass das Geheimnis immer ein Byte-Array ist und eine Länge haben sollte, die für den verwendeten Algorithmus sinnvoll ist. Im Folgenden verwende ich eine zufällige Base64-codierte Zeichenfolge (zur besseren Lesbarkeit), die in ein Byte-Array konvertiert wird.

Im Pseudocode sieht es so aus:

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

Solange Sie das Geheimnis kennen, können Sie die Signatur selbst generieren und Ihr Ergebnis mit dem Signaturabschnitt des JWT vergleichen, um sicherzustellen, dass es nicht manipuliert wurde. Technisch gesehen wird ein JWT, das kryptografisch signiert wurde, als JWS bezeichnet. JWTs können auch verschlüsselt werden und werden dann als JWE bezeichnet. (In der Praxis wird der Begriff JWT verwendet, um JWEs und JWSs zu beschreiben.)

Dies bringt uns zurück zu den Vorteilen der Verwendung eines JWT als CSRF-Token. Wir können die Signatur überprüfen und die im JWT codierten Informationen verwenden, um ihre Gültigkeit zu bestätigen. Die Zeichenfolgendarstellung des JWT muss also nicht nur mit den serverseitig gespeicherten Daten übereinstimmen, sondern wir können auch sicherstellen, dass sie nicht abgelaufen ist, indem wir einfach den exp- Anspruch überprüfen . Dies erspart dem Server die Aufrechterhaltung eines zusätzlichen Status.

Nun, wir haben hier viel Boden unter den Füßen. Lassen Sie uns in einen Code eintauchen!

3. Richten Sie das JJWT-Lernprogramm ein

JJWT (//github.com/jwtk/jjwt) ist eine Java-Bibliothek, die die Erstellung und Überprüfung von JSON-Web-Token durchgängig ermöglicht. Für immer kostenlos und Open Source (Apache License, Version 2.0) wurde es mit einer Builder-fokussierten Oberfläche entwickelt, die den größten Teil seiner Komplexität verbirgt.

Die Hauptoperationen bei der Verwendung von JJWT umfassen das Erstellen und Parsen von JWTs. Als nächstes werden wir uns diese Vorgänge ansehen, dann einige erweiterte Funktionen des JJWT kennenlernen und schließlich JWTs als CSRF-Token in einer Spring Security, Spring Boot-Anwendung in Aktion sehen.

Den in den folgenden Abschnitten gezeigten Code finden Sie hier. Hinweis: Das Projekt verwendet Spring Boot von Anfang an, da es einfach mit der API zu interagieren ist, die es verfügbar macht.

Führen Sie Folgendes aus, um das Projekt zu erstellen:

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

Eines der großartigen Dinge an Spring Boot ist, wie einfach es ist, eine Anwendung zu starten. Führen Sie einfach die folgenden Schritte aus, um die JJWT Fun-Anwendung auszuführen:

java -jar target/*.jar 

In dieser Beispielanwendung sind zehn Endpunkte verfügbar (ich verwende httpie, um mit der Anwendung zu interagieren. Sie finden sie hier.)

http localhost:8080
Available commands (assumes httpie - //github.com/jkbrzt/httpie): http //localhost:8080/ This usage message http //localhost:8080/static-builder build JWT from hardcoded claims http POST //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 //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 //localhost:8080/dynamic-builder-compress claim-1=value-1 ... [claim-n=value-n] build DEFLATE compressed JWT from passed in claims http //localhost:8080/parser?jwt= Parse passed in JWT http //localhost:8080/parser-enforce?jwt= Parse passed in JWT enforcing the 'iss' registered claim and the 'hasMotorcycle' custom claim http //localhost:8080/get-secrets Show the signing keys currently in use. http //localhost:8080/refresh-secrets Generate new signing keys and show them. http POST //localhost:8080/set-secrets HS256=base64-encoded-value HS384=base64-encoded-value HS512=base64-encoded-value Explicitly set secrets to use in the application.

In den folgenden Abschnitten werden wir jeden dieser Endpunkte und den in den Handlern enthaltenen JJWT-Code untersuchen.

4. JWTs mit JJWT erstellen

Aufgrund der fließenden Benutzeroberfläche von JJWT besteht die Erstellung des JWT im Wesentlichen aus drei Schritten:

  1. Die Definition der internen Ansprüche des Tokens wie Aussteller, Betreff, Ablauf und ID.
  2. Die kryptografische Signatur des JWT (macht es zu einem JWS).
  3. Die Komprimierung des JWT zu einer URL-sicheren Zeichenfolge gemäß den JWT Compact Serialization-Regeln.

The final JWT will be a three-part base64-encoded string, signed with the specified signature algorithm, and using the provided key. After this point, the token is ready to be shared with the another party.

Here's an example of the JJWT in 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();

This is very similar to the code that's in the StaticJWTController.fixedBuilder method of the code project.

At this point, it's worth talking about a few anti-patterns related to JWTs and signing. If you've ever seen JWT examples before, you've likely encountered one of these signing anti-pattern scenarios:

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

Any of the HS type signature algorithms takes a byte array. It's convenient for humans to read to take a string and convert it to a byte array.

Anti-pattern 1 above demonstrates this. This is problematic because the secret is weakened by being so short and it's not a byte array in its native form. So, to keep it readable, we can base64 encode the byte array.

However, anti-pattern 2 above takes the base64 encoded string and converts it directly to a byte array. What should be done is to decode the base64 string back into the original byte array.

Number 3 above demonstrates this. So, why is this one also an anti-pattern? It's a subtle reason in this case. Notice that the signature algorithm is HS512. The byte array is not the maximum length that HS512 can support, making it a weaker secret than what is possible for that algorithm.

The example code includes a class called SecretService that ensures secrets of the proper strength are used for the given algorithm. At application startup time, a new set of secrets is created for each of the HS algorithms. There are endpoints to refresh the secrets as well as to explicitly set the secrets.

If you have the project running as described above, execute the following so that the JWT examples below match the responses from your project.

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

Now, you can hit the /static-builder endpoint:

http //localhost:8080/static-builder

This produces a JWT that looks like this:

eyJhbGciOiJIUzI1NiJ9. eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIiwiaWF0IjoxNDY2Nzk2ODIyLCJleHAiOjQ2MjI0NzA0MjJ9. kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ

Now, hit:

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

The response has all the claims that we included when we created the 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" }

This is the parsing operation, which we'll get into in the next section.

Now, let's hit an endpoint that takes claims as parameters and will build a custom JWT for us.

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

Note: There's a subtle difference between the hasMotorcycle claim and the other claims. httpie assumes that JSON parameters are strings by default. To submit raw JSON using using httpie, you use the := form rather than =. Without that, it would submit “hasMotorcycle”: “true”, which is not what we want.

Here's the output:

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" } 

Let's take a look at the code that backs this endpoint:

@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); }

Line 2 ensures that the incoming JSON is automatically converted to a Java Map, which is super handy for JJWT as the method on line 5 simply takes that Map and sets all the claims at once.

As terse as this code is, we need something more specific to ensure that the claims that are passed are valid. Using the .setClaims(Map claims) method is handy when you already know that the claims represented in the map are valid. This is where the type-safety of Java comes into the JJWT library.

For each of the Registered Claims defined in the JWT specification, there's a corresponding Java method in the JJWT that takes the spec-correct type.

Let's hit another endpoint in our example and see what happens:

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

Note that we've passed in an integer, 5, for the “sub” claim. Here's the output:

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" }

Now, we're getting an error response because the code is enforcing the type of the Registered Claims. In this case, sub must be a string. Here's the code that backs this endpoint:

@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()); }

Just like before, the method accepts a Map of claims as its parameter. However, this time, we are calling the specific method for each of the Registered Claims which enforces type.

One refinement to this is to make the error message more specific. Right now, we only know that one of our claims is not the correct type. We don't know which claim was in error or what it should be. Here's a method that will give us a more specific error message. It also deals with a bug in the current code.

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); } }

Line 3 checks that the passed in value is of the expected type. If not, a JwtException is thrown with the specific error. Let's take a look at this in action by making the same call we did earlier:

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" }

Now, we have a very specific error message telling us that the sub claim is the one in error.

Let's circle back to that bug in our code. The issue has nothing to do with the JJWT library. The issue is that the JSON to Java Object mapper built into Spring Boot is too smart for our own good.

If there's a method that accepts a Java Object, the JSON mapper will automatically convert a passed in number that is less than or equal to 2,147,483,647 into a Java Integer. Likewise, it will automatically convert a passed in number that is greater than 2,147,483,647 into a Java Long. For the iat, nbf, and exp claims of a JWT, we want our ensureType test to pass whether the mapped Object is an Integer or a Long. That's why we have the additional clause in determining if the passed in value is the correct type:

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

If we're expecting a Long, but the value is an instance of Integer, we still say it's the correct type. With an understanding of what's happening with this validation, we can now integrate it into our dynamicBuilderSpecific method:

@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: In all the example code in this section, JWTs are signed with the HMAC using SHA-256 algorithm. This is to keep the examples simple. The JJWT library supports 12 different signature algorithms that you can take advantage of in your own code.

5. Parsing JWTs With JJWT

We saw earlier that our code example has an endpoint for parsing a JWT. Hitting this endpoint:

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

produces this response:

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" }

The parser method of the StaticJWTController class looks like this:

@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); }

Line 4 indicates that we expect the incoming string to be a signed JWT (a JWS). And, we are using the same secret that was used to sign the JWT in parsing it. Line 5 parses the claims from the JWT. Internally, it is verifying the signature and it will throw an exception if the signature is invalid.

Notice that in this case we are passing in a SigningKeyResolver rather than a key itself. This is one of the most powerful aspects of JJWT. The header of JWT indicates the algorithm used to sign it. However, we need to verify the JWT before we trust it. It would seem to be a catch 22. Let's look at the SecretService.getSigningKeyResolver method:

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

Using the access to the JwsHeader, I can inspect the algorithm and return the proper byte array for the secret that was used to sign the JWT. Now, JJWT will verify that the JWT has not been tampered with using this byte array as the key.

If I remove the last character of the passed in JWT (which is part of the signature), this is the response:

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. JWTs in Practice: Spring Security CSRF Tokens

While the focus of this post is not Spring Security, we are going to delve into it a bit here to showcase some real-world usage of the JJWT library.

Cross Site Request Forgery is a security vulnerability whereby a malicious website tricks you into submitting requests to a website that you have established trust with. One of the common remedies for this is to implement a synchronizer token pattern. This approach inserts a token into the web form and the application server checks the incoming token against its repository to confirm that it is correct. If the token is missing or invalid, the server will respond with an error.

Spring Security has the synchronizer token pattern built in. Even better, if you are using the Spring Boot and Thymeleaf templates, the synchronizer token is automatically inserted for you.

By default, the token that Spring Security uses is a “dumb” token. It's just a series of letters and numbers. This approach is just fine and it works. In this section, we enhance the basic functionality by using JWTs as the token. In addition to verifying that the submitted token is the one expected, we validate the JWT to further prove that the token has not been tampered with and to ensure that it is not expired.

To get started, we are going to configure Spring Security using Java configuration. By default, all paths require authentication and all POST endpoints require CSRF tokens. We are going to relax that a bit so that what we've built so far still works.

@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(); } }

We are doing two things here. First, we are saying the CSRF tokens are not required when posting to our REST API endpoints (line 15). Second, we are saying that unauthenticated access should be allowed for all paths (lines 17 – 18).

Let's confirm that Spring Security is working the way we expect. Fire up the app and hit this url in your browser:

//localhost:8080/jwt-csrf-form

Here's the Thymeleaf template for this view:

This is a very basic form that will POST to the same endpoint when submitted. Notice that there is no explicit reference to CSRF tokens in the form. If you view the source, you will see something like:

This is all the confirmation you need to know that Spring Security is functioning and that the Thymeleaf templates are automatically inserting the CSRF token.

To make the value a JWT, we will enable a custom CsrfTokenRepository. Here's how our Spring Security configuration changes:

@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(); } }

To connect this, we need a configuration that exposes a bean that returns the custom token repository. Here's the configuration:

@Configuration public class CSRFConfig { @Autowired SecretService secretService; @Bean @ConditionalOnMissingBean public CsrfTokenRepository jwtCsrfTokenRepository() { return new JWTCsrfTokenRepository(secretService.getHS256SecretBytes()); } }

And, here's our custom repository (the important bits):

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) { ... } }

The generateToken method creates a JWT that expires 30 seconds after it's created. With this plumbing in place, we can fire up the application again and look at the source of /jwt-csrf-form.

Now, the hidden field looks like this:

Huzzah! Now our CSRF token is a JWT. That wasn't too hard.

However, this is only half the puzzle. By default, Spring Security simply saves the CSRF token and confirms that the token submitted in a web form matches the one that's saved. We want to extend the functionality to validate the JWT and make sure it hasn't expired. To do that, we'll add in a filter. Here's what our Spring Security configuration looks like now:

@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(); } ... }

On line 9, we've added in a filter and we are placing it in the filter chain after the default CsrfFilter. So, by the time our filter is hit, the JWT token (as a whole) will have already been confirmed to be the correct value saved by Spring Security.

Here's the JwtCsrfValidatorFilter (it's private as it's an inner class of our Spring Security configuration):

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); } }

Take a look at line 23 on. We are parsing the JWT as before. In this case, if an Exception is thrown, the request is forwarded to the expired-jwt template. If the JWT validates, then processing continues as normal.

This closes the loop on overriding the default Spring Security CSRF token behavior with a JWT token repository and validator.

If you fire up the app, browse to /jwt-csrf-form, wait a little more than 30 seconds and click the button, you will see something like this:

7. JJWT Extended Features

We'll close out our JJWT journey with a word on some of the features that extend beyond the specification.

7.1. Enforce Claims

As part of the parsing process, JJWT allows you to specify required claims and values those claims should have. This is very handy if there is certain information in your JWTs that must be present in order for you to consider them valid. It avoids a lot of branching logic to manually validate claims. Here's the method that serves the /parser-enforce endpoint of our sample project.

@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); }

Lines 5 and 6 show you the syntax for registered claims as well as custom claims. In this example, the JWT will be considered invalid if the iss claim is not present or does not have the value: Stormpath. It will also be invalid if the custom hasMotorcycle claim is not present or does not have the value: true.

Let's first create a JWT that follows the happy path:

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" }

Now, let's validate that 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" }

So far, so good. Now, this time, let's leave the hasMotorcycle out:

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

This time, if we try to validate the JWT:

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

we get:

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" }

This indicates that our hasMotorcycle claim was expected, but was missing.

Let's do one more example:

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

This time, the required claim is present, but it has the wrong value. Let's see the output of:

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" }

This indicates that our hasMotorcycle claim was present, but had a value that was not expected.

MissingClaimException and IncorrectClaimException are your friends when enforcing claims in your JWTs and a feature that only the JJWT library has.

7.2. JWT Compression

If you have a lot of claims on a JWT, it can get big – so big, that it might not fit in a GET url in some browsers.

Let's a make a big 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

Here's the JWT that produces:

eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIiwidGhlIjoicXVpY2siLCJicm93biI6ImZveCIsImp1bXBlZCI6Im92ZXIiLCJsYXp5IjoiZG9nIiwic29tZXdoZXJlIjoib3ZlciIsInJhaW5ib3ciOiJ3YXkiLCJ1cCI6ImhpZ2giLCJhbmQiOiJ0aGUiLCJkcmVhbXMiOiJ5b3UiLCJkcmVhbWVkIjoib2YifQ.AHNJxSTiDw_bWNXcuh-LtPLvSjJqwDvOOUcmkk7CyZA

That sucker's big! Now, let's hit a slightly different endpoint with the same claims:

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

This time, we get:

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

62 characters shorter! Here's the code for the method used to generate the 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); }

Notice on line 6 we are specifying a compression algorithm to use. That's all there is to it.

What about parsing compressed JWTs? The JJWT library automatically detects the compression and uses the same algorithm to decompress:

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" }

Notice the calg claim in the header. This was automatically encoded into the JWT and it provides the hint to the parser about what algorithm to use for decompression.

NOTE: The JWE specification does support compression. In an upcoming release of the JJWT library, we will support JWE and compressed JWEs. We will continue to support compression in other types of JWTs, even though it is not specified.

8. Token Tools for Java Devs

While the core focus of this article was not Spring Boot or Spring Security, using those two technologies made it easy to demonstrate all the features discussed in this article. You should be able to build in fire up the server and start playing with the various endpoints we've discussed. Just hit:

http //localhost:8080

Stormpath is also excited to bring a number of open source developer tools to the Java community. These include:

8.1. JJWT (What We've Been Talking About)

JJWT is an easy to use tool for developers to create and verify JWTs in Java. Like many libraries Stormpath supports, JJWT is completely free and open source (Apache License, Version 2.0), so everyone can see what it does and how it does it. Do not hesitate to report any issues, suggest improvements, and even submit some code!

8.2. jsonwebtoken.io and java.jsonwebtoken.io

jsonwebtoken.io is a developer tool we created to make it easy to decode JWTs. Simply paste an existing JWT into the appropriate field to decode its header, payload, and signature. jsonwebtoken.io is powered by nJWT, the cleanest free and open source (Apache License, Version 2.0) JWT library for Node.js developers. You can also see code generated for a variety of languages at this website. The website itself is open-source and can be found here.

java.jsonwebtoken.io is specifically for the JJWT library. You can alter the headers and payload in the upper right box, see the JWT generated by JJWT in the upper left box, and see a sample of the builder and parser Java code in the lower boxes. The website itself is open source and can be found here.

8.3. JWT Inspector

The new kid on the block, JWT Inspector is an open source Chrome extension that allows developers to inspect and debug JWTs directly in-browser. The JWT Inspector will discover JWTs on your site (in cookies, local/session storage, and headers) and make them easily accessible through your navigation bar and DevTools panel.

9. JWT This Down!

JWTs add some intelligence to ordinary tokens. The ability to cryptographically sign and verify, build in expiration times and encode other information into JWTs sets the stage for truly stateless session management. This has a big impact on the ability to scale applications.

At Stormpath, we use JWTs for OAuth2 tokens, CSRF tokens and assertions between microservices, among other usages.

Sobald Sie JWTs verwenden, kehren Sie möglicherweise nie mehr zu den dummen Token der Vergangenheit zurück. Haben Sie Fragen? Schlagen Sie mich bei @afitnerd auf Twitter.