How to Implement JSON Web Token (JWT) in Java Spring Boot

JSON Web Token or JWT has been famous as a way to communicate securely between services. There are two form of JWT, JWS and JWE. The difference between them is that JWS' payload is not encrypted while JWE is.

This article will explore the implementation of the JWT in Java Spring Boot. If you want to learn more about the JWT itself, you can visit my other article here.

The code in this article is hosted on the following GitHub repository: https://github.com/brilianfird/jwt-demo.

Library

For this article, we will use the jose4j library. jose4j is one of the popular JWT libraries in Java and has a full feature. If you want to check out other libraries (whether it's for Java or not), jwt.io has compiled a list of them.

<dependency>  
    <groupId>org.bitbucket.b_c</groupId>  
    <artifactId>jose4j</artifactId>  
    <version>0.7.12</version>  
</dependency>

Implementing JWS in Java

JSON Web Signature (JWS) consists of three parts:

  • JOSE Header
  • Payload
  • Signature

Let's see an example of the JOSE header:

{
	alg:"HS264"
}

JOSE header store the metadata about how to handle the JWS.
alg stores information about which signing algorithm the JWT uses.

Next, let's check the payload:

{
  "sub": "1234567890",
  "name": "Brilian Firdaus",
  "iat": 1651422365
}

JSON payload stores the data that we want to transmit to the client. It also stores some JWT claims for information purposes that we can verify.

In the example above, we have three fields registered as JWT claims.

  • sub indicates the user's unique id
  • name indicates the name of the user
  • iat indicates when we created the JWT in an epoch

The last part is the signature, which is the one that makes JWS secure. Usually, the signature of the JWS will be in the form of bytes. Let's see an example of a Base64 Encoded signature:

qsg3HKPxM96PeeXl-sMrao00yOh1T0yQfZa-BsrtjHI

Now, if we see the three parts above, you might wonder how to transfer those three parts seamlessly to the consumer. The answer is with compact serialization. Using compact serialization, we can easily share the JWS with the consumer because the JWS will become one long string.

Base64.encode(JOSE Header) + "." + Base64.encode(Payload) + "." + Base64.encode(signature)


The result will be:

eyJhbGciOiJIUzI1NiIsImtpZCI6IjIwMjItMDUtMDEifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkJyaWxpYW4gRmlyZGF1cyIsImlhdCI6MTY1MTQyMjM2NX0.qsg3HKPxM96PeeXl-sMrao00yOh1T0yQfZa-BsrtjHI
The compact serialization part is also mandatory in the JWT specification. So for a JWS to be considered a JWT, we must do a compact serialization.

Unprotected

The first type of JWS we will explore is an unprotected JWS. People rarely use his type of JWS (Basically just a regular JSON), but let's explore this first to understand the base of the implementation.

Let's start by creating the header. Unlike the previous example where we used the HS256 algorithm, now we will use no algorithm.

Producing Unprotected JWS

@Test  
public void JWS_noAlg() throws Exception {  
  
  JwtClaims jwtClaims = new JwtClaims();  
  jwtClaims.setSubject("7560755e-f45d-4ebb-a098-b8971c02ebef"); // set sub
  jwtClaims.setIssuedAtToNow();  // set iat
  jwtClaims.setExpirationTimeMinutesInTheFuture(10080); // set exp
  jwtClaims.setIssuer("https://codecurated.com"); // set iss
  jwtClaims.setStringClaim("name", "Brilian Firdaus");   // set name
  jwtClaims.setStringClaim("email", "brilianfird@gmail.com");//set email  
  jwtClaims.setClaim("email_verified", true);  //set email_verified
  
  JsonWebSignature jws = new JsonWebSignature();  
  jws.setAlgorithmConstraints(AlgorithmConstraints.NO_CONSTRAINTS);  
  jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.NONE);  
  jws.setPayload(jwtClaims.toJson());  
  
  String jwt = jws.getCompactSerialization(); //produce eyJ.. JWT
  System.out.println("JWT: " + jwt);  
}


Let's see what we did in the code.

  • We set a bunch of claims (sub, iat, exp, iss, name, email, email_verified)
  • We set the signing algorithm to NONE and the algorithm constraint to NO_CONSTRAINT because jose4j will throw an exception because the algorithm lack security
  • We packaged JWS in the compact serialization, which will produce one string containing the JWS. The result is a JWT complied String.

Let's see what output we get by calling the jws.getCompactSerialization():

eyJhbGciOiJub25lIn0.eyJzdWIiOiI3NTYwNzU1ZS1mNDVkLTRlYmItYTA5OC1iODk3MWMwMmViZWYiLCJpYXQiOjE2NTI1NTYyNjYsImV4cCI6MTY1MzE2MTA2NiwiaXNzIjoiaHR0cHM6Ly9jb2RlY3VyYXRlZC5jb20iLCJuYW1lIjoiQnJpbGlhbiBGaXJkYXVzIiwiZW1haWwiOiJicmlsaWFuZmlyZEBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZX0.

If we try to decode it, we'll get the JWS with fields that we set before:

{
  "header": {
    "alg": "none"
  },
  "payload": {
    "sub": "7560755e-f45d-4ebb-a098-b8971c02ebef",
    "iat": 1652556266,
    "exp": 1653161066,
    "iss": "https://codecurated.com",
    "name": "Brilian Firdaus",
    "email": "brilianfird@gmail.com",
    "email_verified": true
  }
}

We've successfully created a JWT with Java's jose4j library! Now, let's proceed to the JWT-consuming process.

To consume the JWT, we can use the JwtConsumer class in the jose4j library. Let's see an example:

@Test  
public void JWS_consume() throws Exception {  
  String jwt = "eyJhbGciOiJub25lIn0.eyJzdWIiOiI3NTYwNzU1ZS1mNDVkLTRlYmItYTA5OC1iODk3MWMwMmViZWYiLCJpYXQiOjE2NTI1NTYyNjYsImV4cCI6MTY1MzE2MTA2NiwiaXNzIjoiaHR0cHM6Ly9jb2RlY3VyYXRlZC5jb20iLCJuYW1lIjoiQnJpbGlhbiBGaXJkYXVzIiwiZW1haWwiOiJicmlsaWFuZmlyZEBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZX0.";  
  
  JwtConsumer jwtConsumer = new JwtConsumerBuilder()  
		  // required for NONE alg  
          .setJwsAlgorithmConstraints(AlgorithmConstraints.NO_CONSTRAINTS) 
          // disable signature requirement  
          .setDisableRequireSignature()
          // require the JWT to have iat field  
          .setRequireIssuedAt() 
          // require the JWT to have exp field 
          .setRequireExpirationTime()  
          // expect the iss to be https://codecurated.com  
          .setExpectedIssuer("https://codecurated.com") 
          .build();  
          
  // process JWT to jwt context  
  JwtContext jwtContext = jwtConsumer.process(jwt); 
  // get JWS object
  JsonWebSignature jws = (JsonWebSignature)jwtContext.getJoseObjects().get(0);
  // get claims  
  JwtClaims jwtClaims = jwtContext.getJwtClaims(); 
  
  // print claims as map  
  System.out.println(jwtClaims.getClaimsMap()); 
}

By using JwtConsumer, we can easily make rules about what to validate when processing incoming JWT. It also provides an easy way to get the JWS Object and the claims by using .getJoseObjects() and getJwtClaims(), respectively.

Now that we know how to produce and consume JWT without a signing algorithm, it will be much easier to understand the one with it. The difference is that we need to set the algorithm and create a key(s) to generate/validate the JWT.

HMAC SHA-256

HMAC SHA-256(HS256) is a MAC function with a symmetric key. We will need to generate at least 32 bytes for its secret key and feed it to the HmacKey class in the jose4j library to ensure security.

We'll use the SecureRandom library in Java to ensure the key randomity.

byte[] key = new byte[32];  
  
SecureRandom secureRandom = new SecureRandom();  
secureRandom.nextBytes(key);

HmacKey hmacKey = new HmacKey(key);
The secret key should be considered as a credential, hence it should be stored in a secure environment. For recommendation, you can store it as a environment variable or in [Vault](https://www.vaultproject.io/).

Let's see how to create and consume the JWT signed with HS256:

@Test  
public void JWS_HS256() throws Exception {  
    
  // generate  key  
  byte[] key = new byte[32];  
  SecureRandom secureRandom = new SecureRandom();  
  secureRandom.nextBytes(key);  
  HmacKey hmacKey = new HmacKey(key);  
  
  JwtClaims jwtClaims = new JwtClaims();  
  jwtClaims.setSubject("7560755e-f45d-4ebb-a098-b8971c02ebef"); // set sub  
  jwtClaims.setIssuedAtToNow();  // set iat  
  jwtClaims.setExpirationTimeMinutesInTheFuture(10080); // set exp  
  jwtClaims.setIssuer("https://codecurated.com"); // set iss  
  jwtClaims.setStringClaim("name", "Brilian Firdaus");   // set name  
  jwtClaims.setStringClaim("email", "brilianfird@gmail.com");//set email  
  jwtClaims.setClaim("email_verified", true);  //set email_verified  
  
  JsonWebSignature jws = new JsonWebSignature();  
  // Set alg header as HMAC_SHA256  
  jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.HMAC_SHA256);  
  // Set key to hmacKey  
  jws.setKey(hmacKey);  
  jws.setPayload(jwtClaims.toJson());  
  
  String jwt = jws.getCompactSerialization(); //produce eyJ.. JWT  
  
  // we don't need NO_CONSTRAINT and disable require signature anymore 
  JwtConsumer jwtConsumer = new JwtConsumerBuilder()  
          .setRequireIssuedAt()  
          .setRequireExpirationTime()  
          .setExpectedIssuer("https://codecurated.com")  
          // set the verification key  
          .setVerificationKey(hmacKey)  
          .build();  
  
  // process JWT to jwt context  
  JwtContext jwtContext = jwtConsumer.process(jwt);  
  // get JWS object  
  JsonWebSignature consumedJWS = (JsonWebSignature)jwtContext.getJoseObjects().get(0);  
  // get claims  
  JwtClaims consumedJWTClaims = jwtContext.getJwtClaims();  
  
  // print claims as map  
  System.out.println(consumedJWTClaims.getClaimsMap());  
  
  // Assert header, key, and claims  
  Assertions.assertEquals(jws.getAlgorithmHeaderValue(), consumedJWS.getAlgorithmHeaderValue());  
  Assertions.assertEquals(jws.getKey(), consumedJWS.getKey());  
  Assertions.assertEquals(jwtClaims.toJson(), consumedJWTClaims.toJson());  
}

There isn't much difference in the code compared to creating a JWS without a signing algorithm. We first made the key using SecureRandom and HmacKey classes. Since HS256 uses a symmetric key, we only need one key that we will use to sign and verify the JWT.

We also set the algorithm header value to HS256 by using jws.setAlgorithmheaderValue(AlgorithmIdentifiers.HMAC_SHA256 and the key with jws.setKey(hmacKey).

In the JWT consumer, we only need to set the HMAC key by using .setVerificationKey(hmacKey) on the jwtConsumer object jose4j will automatically determine which algorithm is used in the JWS by parsing its JOSE header.

ES256

Unlike the HS256 that only needs one key, we need to generate two keys for the ES256 algorithm, private and public keys.

We can use the private key to create and verify the JWT, while we can only use public keys to verify the JWT. Due to those traits, a private key is usually stored as a credential, while a public key can be hosted in public as JWK so the consumer of the JWT can query the host and get the key by themself.

jose4j library provides a simple API to generate private and public keys as a JWK.

EllipticCurveJsonWebKey ellipticCurveJsonWebKey = EcJwkGenerator.generateJwk(EllipticCurves.P256);

// get private key
ellipticCurveJsonWebKey.getPrivateKey();

// get public key
ellipticCurveJsonWebKey.getECPublicKey();

Now that we know how to generate the key creating the JWT with the ES256 algorithm is almost the same as creating a JWT with the HS256 algorithm.

...
JsonWebSignature jws = new JsonWebSignature();  
// Set alg header as ECDSA_USING_P256_CURVE_AND_SHA256  
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256);  
// Set key to the generated private key  
jws.setKey(ellipticCurveJsonWebKey.getPrivateKey());  
jws.setPayload(jwtClaims.toJson());
...
JwtConsumer jwtConsumer = new JwtConsumerBuilder()  
        .setRequireIssuedAt()  
        .setRequireExpirationTime()  
        .setExpectedIssuer("https://codecurated.com")  
        // set the verification key as the public key  
        .setVerificationKey(ellipticCurveJsonWebKey.getECPublicKey())  
        .build();
...

The only different things are:

  • We set the algorithm header as ECDSA_USING_P256_CURVE_AND_SHA256
  • We use the private key when creating the JWT
  • We use the public key for verifying the JWT

Hosting JWK

We can easily create JSON Web Key Set using the JsonWebKeySet class.

@GetMapping("/jwk")  
public String jwk() throws JoseException {  
// Create public key and private key pair
  EllipticCurveJsonWebKey ellipticCurveJsonWebKey = EcJwkGenerator.generateJwk(EllipticCurves.P256);  
  
  // Create JsonWebkeySet object
  JsonWebKeySet jsonWebKeySet = new JsonWebKeySet();  

  // Add the public key to the JsonWebKeySet object
  jsonWebKeySet.addJsonWebKey(ellipticCurveJsonWebKey);  

  // toJson() method by default won't host the private key
  return jsonWebKeySet.toJson();  
}

We also need to change some properties of the key resolver:

// Define verification key resolver
HttpsJwks httpsJkws = new HttpsJwks("http://localhost:8080/jwk");  
HttpsJwksVerificationKeyResolver verificationKeyResolver =  
    new HttpsJwksVerificationKeyResolver(httpsJkws);  
  
JwtConsumer jwtConsumer = new JwtConsumerBuilder()  
    .setRequireIssuedAt()  
    .setRequireExpirationTime()  
    .setExpectedIssuer("https://codecurated.com")  
    // set verification key resolver
    .setVerificationKeyResolver(verificationKeyResolver)  
    .build();

Since we hosted the JSON Web Key Set, we need to query the host. jose4j is also providing a simple way to do this by using HttpsJwksVerificationKeyResolver.

Implementing JWE in Java

JSON Web Encryption, unlike JWS, is a type of JWT that is encrypted so that no one can see its content except the one with the private key. First, let's see an example of it.

eyJhbGciOiJFQ0RILUVTK0EyNTZLVyIsImVuYyI6IkExMjhDQkMtSFMyNTYiLCJlcGsiOnsia3R5IjoiRUMiLCJ4IjoiMEdxMEFuWUk1RVFxOUVZYjB4dmxjTGxKanV6ckxhSjhUYUdHYzk5MU9sayIsInkiOiJya1Q2cjlqUWhjRU1xaGtubHJ6S0hVemFKMlhWakFpWGpIWGZYZU9aY0hRIiwiY3J2IjoiUC0yNTYifX0.DUrC7Y_ejpt1n9c8wXetwU65sxkEYxG6RBsCUdokVODJBtwypL9VjQ.ydZx-UDWDN7jbGeESXvPHg.6ksHUeeGgGj0txFNXmsSQUCnAv52tJuGR5vgrX54vnLkryPFv2ATdLwYXZz3mAjeDes4s9otz4-Fzg1IBZ4qsfCVa6_3CVdkb8BTU4OvQx23SFEgtj8zh-8ZrqZbpKIT.p-E09mQIleNCCmwX3YL-uQ

The structure of the JWE is:

BASE64URL(UTF8(JWE Protected Header)) || ’.’ ||
BASE64URL(JWE Encrypted Key) || ’.’ ||
BASE64URL(JWE Initialization Vector) || ’.’ ||
BASE64URL(JWE Ciphertext) || ’.’ ||
BASE64URL(JWE Authentication Tag)

And if we decrypt the JWE, we will get the following claims:

{
	"iss":"https://codecurated.com",
	"exp":1654274573,
	"iat":1654256573,
	"sub":"12345"
}

Now, let's see how we create the JWE:

@Test  
public void JWE_ECDHES256() throws Exception {  
  // Determine signature algorithm and encryption algorithm  
  String alg = KeyManagementAlgorithmIdentifiers.ECDH_ES_A256KW;  
  String encryptionAlgorithm = ContentEncryptionAlgorithmIdentifiers.AES_128_CBC_HMAC_SHA_256;  
  
  // Generate EC JWK  
  EllipticCurveJsonWebKey ecJWK = EcJwkGenerator.generateJwk(EllipticCurves.P256);  
  
  // Create  
  JwtClaims jwtClaims = new JwtClaims();  
  jwtClaims.setIssuer("https://codecurated.com");  
  jwtClaims.setExpirationTimeMinutesInTheFuture(300);  
  jwtClaims.setIssuedAtToNow();  
  jwtClaims.setSubject("12345");  
  
  // Create JWE  
  JsonWebEncryption jwe = new JsonWebEncryption();  
  jwe.setPlaintext(jwtClaims.toJson());  
  
  // Set JWE's signature algorithm and encryption algorithm  
  jwe.setAlgorithmHeaderValue(alg);  
  jwe.setEncryptionMethodHeaderParameter(encryptionAlgorithm);  
  
  // Unlike JWS, to create the JWE we use the public key  
  jwe.setKey(ecJWK.getPublicKey());  
  String compactSerialization = jwe.getCompactSerialization();  
  System.out.println(compactSerialization);  
  
  // Create JWT Consumer  
  JwtConsumer jwtConsumer =  
      new JwtConsumerBuilder()  
          // We set the private key as decryption key  
          .setDecryptionKey(ecJWK.getPrivateKey())  
          // JWE doesn't have signature, so we disable it  
          .setDisableRequireSignature()  
          .build();  
  
  // Get the JwtContext of the JWE  
  JwtContext jwtContext = jwtConsumer.process(compactSerialization);  
  
  System.out.println(jwtContext.getJwtClaims());  
}

The main difference between creating and consuming JWE compared to JWS are:

  • We use a public key as the encryption key and a private key as the decryption key
  • We don't have a signature in JWE, so the consumer will need to skip the signature requirement

Conclusion

In this article, we've learned to create both JWS and JWE in Java using jose4j. Hopefully, this article is useful to you. If you want to learn more about the concept of JWT, you can visit my other article.