JSON Web Token or JWT has been popular as a way to communicate securely between services. There are two form of JWT, JWS and JWE. This article will explore the implementation of the 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.
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>
JSON Web Signature (JWS) consists of three parts:
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 idname
indicates the name of the useriat
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.
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", "[email protected]");//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.
sub
, iat
, exp
, iss
, name
, email
, email_verified
)NONE
and the algorithm constraint to NO_CONSTRAINT
because jose4j
will throw an exception because the algorithm lack security
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": "[email protected]",
"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(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", "[email protected]");//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.
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:
ECDSA_USING_P256_CURVE_AND_SHA256
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
.
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:
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.
Also published here.