JWT Verification and Signing in Java


Posted by Steven

The first article in this series about JWT gave an introduction to important certificate file formats while the second article explained JWT in general. This final article in the miniseries includes more information about how signing JWTs works, including code snippets for Java.

The complete source code of this article can be found here.

Signing JWT in General

A signed JWT consists of a header part, a payload and a signature part at the end, all separated by a period:

  1. eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
  2. eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
  3. SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

(line breaks only for brevity)

So, signing a token means to generate an additional string that gets appended to the unsigned token which consists only of the header and the payload. This additional string is calculated using hashing algorithms and signing algorithms.

Signing Algorithms and Hashing Algorithms

When signing a JWT, the algorithms to be used have to be specified. Here is an example for creating a signed JWT with the JWT implementation of Auth0:

  1. RSAKeyProvider provider = ... // specify where to find private and public key
  2. Algorithm algorithm = Algorithm.RSA256(provider);
  3. String token = JWT.create()
  4. .withIssuer("auth0")
  5. .sign(algorithm);

In this example, the algorithm was specified as "RSA256". This string actually includes two algorithms, not just one. It is a combination of a **signature algorithm** and a **hashing algorithm**. A JWT cannot be signed directly because it is often too long for the signing algorithm. This is solved by not signing the JWT directly, but sign the hash of the JWT instead. The hash has a fixed, small size and can be used as input for the signature algorithm. (source)

Often, hashing algorithms of the SHA-family are used.

Examples of signing algorithms are the symmetric HMAC or the asymmetric RSASSA-PKCS1-v1_5.

Here is a table of the supported algorithms of the JWT implementation from Auth0, taken from the official Github repository of Auth0:

JWS Algorithm Description
HS256 HMAC256 HMAC with SHA-256
HS384 HMAC384 HMAC with SHA-384
HS512 HMAC512 HMAC with SHA-512
RS256 RSA256 RSASSA-PKCS1-v1_5 with SHA-256
RS384 RSA384 RSASSA-PKCS1-v1_5 with SHA-384
RS512 RSA512 RSASSA-PKCS1-v1_5 with SHA-512
ES256 ECDSA256 ECDSA with curve P-256 and SHA-256
ES256K ECDSA256 ECDSA with curve secp256k1 and SHA-256
ES384 ECDSA384 ECDSA with curve P-384 and SHA-384
ES512 ECDSA512 ECDSA with curve P-521 and SHA-512

The third column describes the combination of signature algorithm and hashing algorithm.

That table elaborates the above Java example: We used a key that was signed with RSASSA-PKCS1-v1_5 with the hash algorithm of SHA-256.

Sign JWT with symmetric HMAC

The following snippet shows how to sign a JWT with a symmetric HMAC algorithm. To validate the JWT, the receiver has to know the secret which has to be transmitted in a save manner.

  1. @Test
  2. void signTokenWithSymmetricHMAC() throws UnsupportedEncodingException {
  3.  
  4. Algorithm algorithm = Algorithm.HMAC256("secret");
  5.  
  6. String token = JWT.create()
  7. .withIssuer("auth0")
  8. .sign(algorithm);
  9.  
  10. assertEquals(
  11. "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." +
  12. "eyJpc3MiOiJhdXRoMCJ9." +
  13. "izVguZPRsBQ5Rqw6dhMvcIwy8_9lQnrO3vpxGwPCuzs",
  14. token);
  15.  
  16. JWTVerifier verifier = JWT.require(algorithm)
  17. .withIssuer("auth0")
  18. .build();
  19.  
  20. verifier.verify(token);
  21.  
  22. assertThrows(JWTVerificationException.class, () -> verifier.verify(token + "x"));
  23. }

Sign JWT with asymmetric RSA

The RSA algorithm doesn't need a shared secret between sender and receiver because the receiver can verify the token with the public key of the sender.

  1. @Test
  2. void signTokenWithAsymmetricRSA() throws IOException {
  3.  
  4. /*
  5.   Creating keys:
  6.  
  7.   // Create RSA-key in PKCS1 format (header "-----BEGIN RSA PRIVATE KEY-----")
  8.   openssl genrsa -out private_key_in_pkcs1.pem 512
  9.  
  10.   // Convert to PKCS8 format (header "-----BEGIN PRIVATE KEY-----")
  11.   openssl pkcs8 -topk8 -in private_key_in_pkcs1.pem -outform pem -nocrypt -out private_key_in_pkcs8.pem
  12.  
  13.   // Extract public key:
  14.   openssl rsa -in private_key_in_pkcs8.pem -pubout > public.pub
  15.  
  16.   */
  17.  
  18. String filepathPrivateKey = "src/test/resources/asymmetric_rsa/private_key_in_pkcs8.pem";
  19. String filepathPublicKey = "src/test/resources/asymmetric_rsa/public.pub";
  20.  
  21. RSAPrivateKey privateKey = (RSAPrivateKey) PemUtils.readPrivateKeyFromFile(filepathPrivateKey, "RSA");
  22. RSAKeyProvider provider = mock(RSAKeyProvider.class);
  23. when(provider.getPrivateKeyId()).thenReturn("my-key-id");
  24. when(provider.getPrivateKey()).thenReturn(privateKey);
  25. when(provider.getPublicKeyById("my-key-id")).thenReturn(
  26. (RSAPublicKey) PemUtils.readPublicKeyFromFile(filepathPublicKey, "RSA"));
  27.  
  28. Algorithm algorithm = Algorithm.RSA256(provider);
  29.  
  30. String token = JWT.create()
  31. .withIssuer("auth0")
  32. .sign(algorithm);
  33.  
  34. // Notice how the payload is similar to the test with HMAC signing above:
  35. assertEquals(
  36. "eyJraWQiOiJteS1rZXktaWQiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9." +
  37. "eyJpc3MiOiJhdXRoMCJ9." +
  38. "bzF8jNol1SwVS93t6_02KuDFAmnj8FrBRx9lFqH-Ianlx0Ig0wsx3Xz_6g4HqFYzTKoWIPXvNf8hP1tJqP-h5g",
  39. token);
  40.  
  41. JWTVerifier verifier = JWT.require(algorithm)
  42. .withIssuer("auth0")
  43. .build();
  44.  
  45. verifier.verify(token);
  46.  
  47. assertThrows(JWTVerificationException.class, () -> verifier.verify(token + "x"));
  48. }

Signing and Encrypting a JWT asymmetrically

Last, here is an example of how to first sign and then encrypt a JWT. As stated above, the complete source code can be found here.

  1. @Test
  2. void creatingAnRSASignedAndRSAEncryptedJWT() throws IOException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
  3.  
  4. /*
  5.   According to https://stackoverflow.com/questions/34235875/should-jwt-web-token-be-encrypted, it is
  6.   recommended to first sign the JWT and then encrypt it.
  7.   */
  8.  
  9.  
  10. /*
  11.   1. Creating keys for signing:
  12.  
  13.   // Create RSA-key in PKCS1 format (header "-----BEGIN RSA PRIVATE KEY-----")
  14.   openssl genrsa -out signing_private_key_in_pkcs1.pem 512
  15.  
  16.   // Convert to PKCS8 format (header "-----BEGIN PRIVATE KEY-----")
  17.   openssl pkcs8 -topk8 -in signing_private_key_in_pkcs1.pem -outform pem -nocrypt -out signing_private_key_in_pkcs8.pem
  18.  
  19.   // Extract public key:
  20.   openssl rsa -in signing_private_key_in_pkcs8.pem -pubout > signing_public.pub
  21.  
  22.   These keys are used for signing. Hence, the creator of the JWT only publishes his public key for
  23.   validation of the JWT that he signs with his private key.
  24.   */
  25.  
  26. // 2. Create JWT and sign it
  27.  
  28. String filepathSigningPrivateKey = "src/test/resources/signedAndEncrypted/signing_private_key_in_pkcs8.pem";
  29. String filepathSigningPublicKey = "src/test/resources/signedAndEncrypted/signing_public.pub";
  30.  
  31. RSAPrivateKey signingPrivateKey = (RSAPrivateKey) PemUtils.readPrivateKeyFromFile(filepathSigningPrivateKey, "RSA");
  32. RSAKeyProvider provider = mock(RSAKeyProvider.class);
  33. when(provider.getPrivateKeyId()).thenReturn("my-key-id");
  34. when(provider.getPrivateKey()).thenReturn(signingPrivateKey);
  35. when(provider.getPublicKeyById("my-key-id")).thenReturn(
  36. (RSAPublicKey) PemUtils.readPublicKeyFromFile(filepathSigningPublicKey, "RSA"));
  37.  
  38. Algorithm algorithm = Algorithm.RSA256(provider);
  39.  
  40. String signedToken = JWT.create()
  41. .withIssuer("auth0")
  42. .withClaim("name", "Bob")
  43. .sign(algorithm);
  44.  
  45. assertEquals("eyJraWQiOiJteS1rZXktaWQiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9." +
  46. "eyJpc3MiOiJhdXRoMCIsIm5hbWUiOiJCb2IifQ." +
  47. "XzRwsAUP2fscy6jYGk5tVGwnjTCA8pyCpsHYZayh4qRfdMbJ6fBasvg0yqx8QPjSnJDCzYoYaFPw5of-33G4dQ",
  48. signedToken);
  49.  
  50. /*
  51.   3. Creating keys for encryption:
  52.  
  53.   These keys are created by the receiver of the JWT. The sender uses the public key of the receiver to
  54.   encrypt the JWT, so that only the receiver can decrypt it.
  55.  
  56.   The key length has to be sufficiently long, see https://stackoverflow.com/questions/10007147/getting-a-illegalblocksizeexception-data-must-not-be-longer-than-256-bytes-when
  57.  
  58.   // Create RSA-key in PKCS1 format (header "-----BEGIN RSA PRIVATE KEY-----")
  59.   openssl genrsa -out encrypt_private_key_in_pkcs1.pem 2048
  60.  
  61.   // Convert to PKCS8 format (header "-----BEGIN PRIVATE KEY-----")
  62.   openssl pkcs8 -topk8 -in encrypt_private_key_in_pkcs1.pem -outform pem -nocrypt -out encrypt_private_key_in_pkcs8.pem
  63.  
  64.   // Extract public key:
  65.   openssl rsa -in encrypt_private_key_in_pkcs8.pem -pubout > encrypt_public.pub
  66.  
  67.   These keys are used for signing. Hence, the creator of the JWT only publishes his public key and keeps
  68.   the secret key hidden.
  69.   */
  70.  
  71.  
  72. // 4. Encrypt signed JWT (implementation from https://www.baeldung.com/java-rsa):
  73.  
  74. String filepathEncryptPrivateKey = "src/test/resources/signedAndEncrypted/encrypt_private_key_in_pkcs8.pem";
  75. String filepathEncryptPublicKey = "src/test/resources/signedAndEncrypted/encrypt_public.pub";
  76.  
  77. RSAPrivateKey encryptPrivateKey = (RSAPrivateKey) PemUtils.readPrivateKeyFromFile(
  78. filepathEncryptPrivateKey, "RSA");
  79. RSAPublicKey encryptPublicKey = (RSAPublicKey) PemUtils.readPublicKeyFromFile(
  80. filepathEncryptPublicKey, "RSA");
  81.  
  82. Cipher encryptCipher = Cipher.getInstance("RSA");
  83. encryptCipher.init(Cipher.ENCRYPT_MODE, encryptPublicKey);
  84.  
  85. byte[] secretMessageBytes = signedToken.getBytes(StandardCharsets.UTF_8);
  86. byte[] encryptedMessageBytes = encryptCipher.doFinal(secretMessageBytes);
  87.  
  88.  
  89. // 5. Decrypt signed JWT
  90.  
  91. Cipher decryptCipher = Cipher.getInstance("RSA");
  92. decryptCipher.init(Cipher.DECRYPT_MODE, encryptPrivateKey);
  93.  
  94. byte[] decryptedMessageBytes = decryptCipher.doFinal(encryptedMessageBytes);
  95. String decryptedSignedToken = new String(decryptedMessageBytes, StandardCharsets.UTF_8);
  96.  
  97. assertEquals(signedToken, decryptedSignedToken);
  98.  
  99.  
  100. // 6. Verify JWT
  101.  
  102. JWTVerifier verifier = JWT.require(algorithm)
  103. .withIssuer("auth0")
  104. .withClaim("name", "Bob")
  105. .build();
  106.  
  107. verifier.verify(decryptedSignedToken);
  108. }

Further Reading

This is a great article about which signing algorithm to use.

This is a great article about JWT best practices.

Share: