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 Library For this article, we will use the library. 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), has compiled a list of them. jose4j jose4j jwt.io <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. stores information about which signing algorithm the JWT uses. alg 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. indicates the user's unique id sub indicates the name of the user name indicates when we created the JWT in an epoch iat 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 algorithm, now we will use no algorithm. HS256 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. We set a bunch of claims ( , , , , , , ) sub iat exp iss name email email_verified We set the signing algorithm to and the algorithm constraint to because will throw an exception because the algorithm lack security NONE NO_CONSTRAINT jose4j 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": "[email protected]", "email_verified": true } } We've successfully created a JWT with Java's library! Now, let's proceed to the JWT-consuming process. jose4j To consume the JWT, we can use the class in the library. Let's see an example: JwtConsumer jose4j @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 , 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 and , respectively. JwtConsumer .getJoseObjects() getJwtClaims() 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( ) 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 class in the library to ensure security. HS256 HmacKey jose4j We'll use the library in Java to ensure the key randomity. SecureRandom 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 and classes. Since uses a symmetric key, we only need one key that we will use to sign and verify the JWT. SecureRandom HmacKey HS256 We also set the algorithm header value to by using and the key with . HS256 jws.setAlgorithmheaderValue(AlgorithmIdentifiers.HMAC_SHA256 jws.setKey(hmacKey) In the JWT consumer, we only need to set the HMAC key by using on the object will automatically determine which algorithm is used in the JWS by parsing its JOSE header. .setVerificationKey(hmacKey) jwtConsumer jose4j ES256 Unlike the that only needs one key, we need to generate two keys for the algorithm, private and public keys. HS256 ES256 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. library provides a simple API to generate private and public keys as a JWK. jose4j 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 algorithm is almost the same as creating a JWT with the algorithm. ES256 HS256 ... 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 class. JsonWebKeySet @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. is also providing a simple way to do this by using . jose4j 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 . Hopefully, this article is useful to you. If you want to learn more about the concept of JWT, you can visit jose4j my other article. Also published . here