Getting ready to build, or struggling with, secure authentication in your Java application? Unsure of the benefits of using tokens (and specifically JSON web tokens), or how to deploy them? I’m excited to answer these questions, and more, for you in this tutorial!

Before we dive into JSON Web Tokens (JWTs), and the JJWT library (created by Stormpath’s CTO, Les Hazlewood, and maintained by a community of contributors), let’s cover some basics.

1. Authentication vs. Token Authentication

The set of protocols an application uses to confirm user identity is authentication. Applications have traditionally persisted identity through session cookies. This paradigm relies on server-side storage of session IDs, which forces developers to create session storage that’s either unique and server-specific, or implemented as a completely separate session storage layer.

Token authentication was developed to solve problems that server-side session IDs didn’t, and couldn’t. Just like traditional authentication, users present verifiable credentials, but instead of a session ID, they’ll now receive a set of tokens. The initial credentials can be the standard username/password pair, API keys, or even tokens from another service (Stormpath’s API Key Authentication Feature is an example of this).

1.1. Why Tokens?

Very simply, using tokens in place of session IDs can lower our server load, streamline permission management, and provide better tools for supporting a distributed or cloud-based infrastructure. With JWT, this is primarily accomplished through the stateless nature of these types of tokens (more on that below).

Tokens offer a wide variety of applications, including Cross Site Request Forgery (CSRF) protection schemes, OAuth 2.0 interactions, session IDs, and (in cookies) authentication representations. In most cases, standards don’t specify a particular format for tokens. Here’s an example of a typical Spring Security CSRF token in an HTML form:

<input name="_csrf" type="hidden" 
  value="f3f42ea9-3104-4d13-84c0-7bcb68202f16"/>

If we try to post that form without the right CSRF token, we’ll get an error response, and that’s the utility of tokens. The above example is a “dumb” token. Consequently, we can’t glean any inherent meaning from the token itself. This is also where JWTs make a big difference.

2. What’s in a JWT?

JWTs (pronounced “jots”) are URL-safe, encoded, cryptographically signed (sometimes encrypted) strings that we can use as tokens in a variety of applications. Here’s an example using a JWT as a CSRF token:

<input name="_csrf" type="hidden" 
  value="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJlNjc4ZjIzMzQ3ZTM0MTBkYjdlNjg3Njc4MjNiMmQ3MCIsImlhdCI6MTQ2NjYzMzMxNywibmJmIjoxNDY2NjMzMzE3LCJleHAiOjE0NjY2MzY5MTd9.rgx_o8VQGuDa2AqCHSgVOD5G68Ld_YYM7N7THmvLIKc"/>

In this case, we can see that the token is much longer than in our previous example. Just like we saw before, if we submit the form without the token, we’ll get an error response.

So, why JWT?

The above token is cryptographically signed, and can therefore be verified, providing proof that it hasn’t been tampered with. Also, JWTs are encoded with a variety of additional information.

Let’s look at the anatomy of a JWT to better understand how we can squeeze all this goodness out of it. We can see that there are three distinct sections separated by periods (.):

Header

eyJhbGciOiJIUzI1NiJ9

Payload

eyJqdGkiOiJlNjc4ZjIzMzQ3ZTM0MTBkYjdlNjg3Njc4MjNiMmQ3MCIsImlhdC I6MTQ2NjYzMzMxNywibmJmIjoxNDY2NjMzMzE3LCJleHAiOjE0NjY2MzY5MTd9

Signature

rgx_o8VQGuDa2AqCHSgVOD5G68Ld_YYM7N7THmvLIKc

Each section is base64 URL-encoded. This ensures that we can use it safely in an URL (more on this later). Let’s take a closer look at each section individually.

2.1. The Header

If we use base64 to decode the header, we’ll get the following JSON string:

{"alg":"HS256"}

This shows that the JWT was signed with HMAC using SHA-256.

2.2. The Payload

If we decode the payload, we’ll get the following JSON string (formatted for clarity):

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

Within the payload, as we can see, there are a number of keys with values. These keys are called “claims”, and the JWT specification has seven of these specified as “registered” claims:

iss

Issuer

sub

Subject

aud

Audience

exp

Expiration

nbf

Not Before

iat

Issued At

jti

JWT ID

When building a JWT, we can put in any custom claims we wish. The list above simply represents the claims that are reserved both in the key that’s used, and the expected type. Our CSRF has a JWT ID, an “Issued At” time, a “Not Before” time, and an Expiration time. The expiration time is exactly one minute past the issued at time.

2.3. The Signature

Finally, we’ll create the signature section by taking the header and payload together (with the . in between) and passing it through the specified algorithm (in this case, HMAC using SHA-256), along with a known secret. Note that the secret is always a byte array, and should be of a length that makes sense for the algorithm used. Below, we’ll use a random base64 encoded string (for readability) that’s converted into a byte array.

It looks like this in pseudo-code:

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

As long as we know the secret, we can generate the signature ourself, and compare our result to the signature section of the JWT to verify that it hasn’t been tampered with. Technically, a JWT that’s been cryptographically signed is called a JWS. JWTs can also be encrypted, and are then a JWE. In actual practice, we use the term JWT to describe JWEs and JWSs.

This brings us back to the benefits of using a JWT as our CSRF token. We can verify the signature and use the information encoded in the JWT to confirm its validity. The string representation of the JWT needs to match what’s stored server-wide, and we can ensure it’s not expired by inspecting the exp claim. As a result, this saves the server from maintaining additional state.

Now let’s dive into some code.

3. Setup the JJWT Tutorial

JJWT (https://github.com/jwtk/jjwt) is a Java library providing end-to-end JSON Web Token creation and verification. Forever free and open-source (Apache License, Version 2.0), it was designed with a builder-focused interface hiding most of its complexity.

The primary operations in using JJWT involve building and parsing JWTs. First, we’ll look at these operations. Then we’ll get into some extended features of the JJWT. Finally, we’ll see JWTs in action as CSRF tokens in a Spring Security, Spring Boot application.

The code demonstrated in the following sections can be found here. Note: The project uses Spring Boot from the beginning, as it’s easy to interact with the API that it exposes.

One of the great things about Spring Boot is how easy it is to build and fire up an application. To run the JJWT Fun application, we’ll simply do the following:

mvn clean spring-boot:run

This example application exposes ten endpoints (we’re using httpie to interact with the application; it can be found 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=<jwt>
    Parse passed in JWT

  http http://localhost:8080/parser-enforce?jwt=<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.

In the sections that follow, we’ll examine each of these endpoints and the JJWT code contained in the handlers.

4. Building JWTs With JJWT

Because of JJWT’s fluent interface, the creation of the JWT is basically a three-step process:

  1. The definition of the internal claims of the token, like Issuer, Subject, Expiration, and ID
  2. The cryptographic signing of the JWT (making it a JWS)
  3. The compaction of the JWT to a URL-safe string, according to the JWT Compact Serialization rules

The final JWT will be a three-part base64-encoded string, signed with the specified signature algorithm, using the provided key. After this point, we’re ready to share the token with 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 we’ve ever seen JWT examples before, we’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=")
     )
    

All of the HS type signature algorithms take a byte array. Taking a string and converting it to a byte array is convenient for humans to read.

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

However, anti-pattern 2 takes the base64 encoded string and converts it directly to a byte array. Therefore, what we should do is 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 isn’t the maximum length that HS512 can support, making it a weaker secret than what’s possible for that algorithm.

The example code includes a class called SecretService, which ensures we use secrets of the proper strength 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 our project is running as described above, we can execute the following, so that the JWT examples below match the responses from our project:

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

Then we can hit the /static-builder endpoint:

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

This produces a JWT that looks like this:

eyJhbGciOiJIUzI1NiJ9.
eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIiwiaWF0IjoxNDY2Nzk2ODIyLCJleHAiOjQ2MjI0NzA0MjJ9.
kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ

Next, we’ll hit:

http 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.

Then we’ll 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 httpie, we can use the := form, rather than =. Without that, it will submit “hasMotorcycle”: “true”, which isn’t 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<String, Object> 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<String, Object>, 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 pass are valid. Using the .setClaims(Map<String, Object> claims) method is handy when we already know that the claims represented in the map are valid. Consequently, 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 passed in the 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"
}

As we can see, 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<String, Object> 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<String, Object> of claims as its parameter. However, this time, we’re 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 isn’t the correct type. We don’t know which claim was in error, or what it should be. So 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);
    }
}

In addition, Line 3 checks that the passed in value is of the expected type. If not, it will throw a JwtException with the specific error. Next, we’ll take a look at this in action by making the same call as 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.

Finally, 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 its 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. Now that we understand what’s happening with this validation, we can integrate it into our dynamicBuilderSpecific method:

@RequestMapping(value = "/dynamic-builder-specific", method = POST)
public JwtResponse dynamicBuilderSpecific(@RequestBody Map<String, Object> 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 the SHA-256 algorithm. This is to keep the examples simple. However, the JJWT library supports 12 different signature algorithms that we can take advantage of in our own code.

5. Parsing JWTs With JJWT

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

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

we get 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<Claims> 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). We’re also using the same secret we used to sign the JWT in parsing it. Line 5 parses the claims from the JWT. Internally, it’s verifying the signature, and will throw an exception if the signature is invalid.

Notice that, in this case, we’re passing in a SigningKeyResolver, rather than a key itself. This is one of the most powerful aspects of JJWT. Furthermore, the header of JWT indicates the algorithm used to sign it. However, we need to verify the JWT before we trust it. It seems 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, we can inspect the algorithm, and return the proper byte array for the secret we used to sign the JWT. Now, using this byte array as the key, JJWT will verify that the JWT wasn’t tampered with.

If we 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 isn’t Spring Security, we’ll 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 us into submitting requests to a website that we have established trust with. One of the common solutions 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’s 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 we’re using the Spring Boot and Thymeleaf templates, the synchronizer token is automatically inserted for us.

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

To start, we’ll use Java configuration to configure Spring Security. By default, all paths require authentication, and all POST endpoints require CSRF tokens. We’ll relax that a bit, so that what we’ve built so far still works:

@Configuration
public class WebSecurityConfig {

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

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf()
                .ignoringAntMatchers(ignoreCsrfAntMatchers)
            .and().authorizeRequests()
                .antMatchers("/**")
                .permitAll();
        return http.build();
    }
}

We’re doing two things here. First, we’re saying the CSRF tokens are not required when posting to our REST API endpoints (line 15). Next, we’re 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. Let’s fire up the app, and enter this url in our browser:

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

Here’s the Thymeleaf template for this view:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <!--/*/ <th:block th:include="fragments/head :: head"/> /*/-->
    </head>
    <body>
        <div class="container-fluid">
            <div class="row">
                <div class="box col-md-6 col-md-offset-3">
                    <p/>
                    <form method="post" th:action="@{/jwt-csrf-form}">
                        <input type="submit" class="btn btn-primary" value="Click Me!"/>
                    </form>
                </div>
            </div>
        </div>
    </body>
</html>

This is a very basic form that will POST to the same endpoint when submitted. Furthermore, we’ll notice that there’s no explicit reference to CSRF tokens in the form. If we view the source, we’ll see something like:

<input type="hidden" name="_csrf" value="5f375db2-4f40-4e72-9907-a290507cb25e" />

This is all the confirmation we 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’ll enable a custom CsrfTokenRepository. Here’s how our Spring Security configuration changes:

@Configuration
public class WebSecurityConfig {

    @Autowired
    CsrfTokenRepository jwtCsrfTokenRepository;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf()
                .csrfTokenRepository(jwtCsrfTokenRepository)
                .ignoringAntMatchers(ignoreCsrfAntMatchers)
            .and().authorizeRequests()
                .antMatchers("/**")
                .permitAll();
        return http.build();
    }
}

Then, 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 we create it. 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:

<input type="hidden" name="_csrf" 
  value="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxZjIyMDdiNTVjOWM0MjU0YjZlMjY4MjQwYjIwNzZkMSIsImlhdCI6MTQ2NzA3MDQwMCwibmJmIjoxNDY3MDcwNDAwLCJleHAiOjE0NjcwNzA0MzB9.2kYLO0iMWUheAncXAzm0UdQC1xUC5I6RI_ShJ_74e5o" />

Huzzah! Now our CSRF token is a JWT, which 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 and the saved token match. 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 {

    ...

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .addFilterAfter(new JwtCsrfValidatorFilter(), CsrfFilter.class)
            .csrf()
                .csrfTokenRepository(jwtCsrfTokenRepository)
                .ignoringAntMatchers(ignoreCsrfAntMatchers)
            .and().authorizeRequests()
                .antMatchers("/**")
                .permitAll();
        return http.build();
    }

    ...
}

On line 9, we added in a filter and placed it in the filter chain after the default CsrfFilter. By the time our filter is hit, the JWT token (as a whole) will already be confirmed as 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 from line 23 on. We’re 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 we fire up the app, browse to /jwt-csrf-form, wait a little more than 30 seconds, and then click the button, we’ll see something like this:

jwt_expired

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 us to specify required claims and the values those claims should have. This is very handy if there’s certain information in our JWTs that must be present in order for us 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<Claims> jws = Jwts.parser()
        .requireIssuer("Stormpath")
        .require("hasMotorcycle", true)
        .setSigningKeyResolver(secretService.getSigningKeyResolver()).build()
        .parseClaimsJws(jwt);

    return new JwtResponse(jws);
}

Lines 5 and 6 show us the syntax for registered claims, as well as custom claims. In this example, the JWT is invalid if the iss claim isn’t present, or doesn’t have the value Stormpath. It will also be invalid if the custom hasMotorcycle claim isn’t present, or doesn’t 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

Then if we try to validate the JWT:

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

we’ll 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 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:

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 is present, but has an unexpected value.

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

7.2. JWT Compression

If we have a lot of claims on a JWT, it can get big; so big, in fact, 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’s quite 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’ll get:

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

It’s 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<String, Object> claims) 
  throws UnsupportedEncodingException {
    String jws =  Jwts.builder()
        .setClaims(claims)
        .compressWith(new DeflateCompressionAlgorithm())
        .signWith(
            SignatureAlgorithm.HS256,
            secretService.getHS256SecretBytes()
        )
        .compact();
    return new JwtResponse(jws);
}

Notice on line 6 that we’re 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 is automatically encoded into the JWT, and it provides the parser with a hint regarding which algorithm to use for decompression.

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

8. Token Tools for Java Devs

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

http http://localhost:8080

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. JJWT is completely free and open source (Apache License, Version 2.0), so everyone can see what it does and how it does it. Don’t 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. We 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. We 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 our site (in cookies, local/session storage, and headers) and make them easily accessible through our 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 uses.

Once you start using JWTs, you may never go back to the dumb tokens of the past. Have any questions? Hit me up at @afitnerd on twitter.


» 下一篇: Guava AssertJ 的使用