Mastodon

JWT Verification and Signing in Java

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:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
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:

RSAKeyProvider provider = ... // specify where to find private and public key
Algorithm algorithm = Algorithm.RSA256(provider);
String token = JWT.create()
  .withIssuer("auth0")
  .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:

JWSAlgorithmDescription
HS256HMAC256HMAC with SHA-256
HS384HMAC384HMAC with SHA-384
HS512HMAC512HMAC with SHA-512
RS256RSA256RSASSA-PKCS1-v1_5 with SHA-256
RS384RSA384RSASSA-PKCS1-v1_5 with SHA-384
RS512RSA512RSASSA-PKCS1-v1_5 with SHA-512
ES256ECDSA256ECDSA with curve P-256 and SHA-256
ES256KECDSA256ECDSA with curve secp256k1 and SHA-256
ES384ECDSA384ECDSA with curve P-384 and SHA-384
ES512ECDSA512ECDSA 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.

@Test
void signTokenWithSymmetricHMAC() throws UnsupportedEncodingException {

    Algorithm algorithm = Algorithm.HMAC256("secret");

    String token = JWT.create()
            .withIssuer("auth0")
            .sign(algorithm);

    assertEquals(
            "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." +
                    "eyJpc3MiOiJhdXRoMCJ9." +
                    "izVguZPRsBQ5Rqw6dhMvcIwy8_9lQnrO3vpxGwPCuzs",
            token);

    JWTVerifier verifier = JWT.require(algorithm)
            .withIssuer("auth0")
            .build();

    verifier.verify(token);

    assertThrows(JWTVerificationException.class, () -> verifier.verify(token + "x"));
}

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.

@Test
void signTokenWithAsymmetricRSA() throws IOException {

    /*
        Creating keys:

        // Create RSA-key in PKCS1 format (header "-----BEGIN RSA PRIVATE KEY-----")
        openssl genrsa -out private_key_in_pkcs1.pem 512

        // Convert to PKCS8 format (header "-----BEGIN PRIVATE KEY-----")
        openssl pkcs8 -topk8 -in private_key_in_pkcs1.pem -outform pem -nocrypt -out private_key_in_pkcs8.pem

        // Extract public key:
        openssl rsa -in private_key_in_pkcs8.pem -pubout > public.pub

     */

    String filepathPrivateKey = "src/test/resources/asymmetric_rsa/private_key_in_pkcs8.pem";
    String filepathPublicKey = "src/test/resources/asymmetric_rsa/public.pub";

    RSAPrivateKey privateKey = (RSAPrivateKey) PemUtils.readPrivateKeyFromFile(filepathPrivateKey, "RSA");
    RSAKeyProvider provider = mock(RSAKeyProvider.class);
    when(provider.getPrivateKeyId()).thenReturn("my-key-id");
    when(provider.getPrivateKey()).thenReturn(privateKey);
    when(provider.getPublicKeyById("my-key-id")).thenReturn(
            (RSAPublicKey) PemUtils.readPublicKeyFromFile(filepathPublicKey, "RSA"));

    Algorithm algorithm = Algorithm.RSA256(provider);

    String token = JWT.create()
            .withIssuer("auth0")
            .sign(algorithm);

    // Notice how the payload is similar to the test with HMAC signing above:
    assertEquals(
            "eyJraWQiOiJteS1rZXktaWQiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9." +
                    "eyJpc3MiOiJhdXRoMCJ9." +
                    "bzF8jNol1SwVS93t6_02KuDFAmnj8FrBRx9lFqH-Ianlx0Ig0wsx3Xz_6g4HqFYzTKoWIPXvNf8hP1tJqP-h5g",
            token);

    JWTVerifier verifier = JWT.require(algorithm)
            .withIssuer("auth0")
            .build();

    verifier.verify(token);

    assertThrows(JWTVerificationException.class, () -> verifier.verify(token + "x"));
}

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.

@Test
void creatingAnRSASignedAndRSAEncryptedJWT() throws IOException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {

    /*
        According to https://stackoverflow.com/questions/34235875/should-jwt-web-token-be-encrypted, it is
        recommended to first sign the JWT and then encrypt it.
     */


    /*
        1. Creating keys for signing:

        // Create RSA-key in PKCS1 format (header "-----BEGIN RSA PRIVATE KEY-----")
        openssl genrsa -out signing_private_key_in_pkcs1.pem 512

        // Convert to PKCS8 format (header "-----BEGIN PRIVATE KEY-----")
        openssl pkcs8 -topk8 -in signing_private_key_in_pkcs1.pem -outform pem -nocrypt -out signing_private_key_in_pkcs8.pem

        // Extract public key:
        openssl rsa -in signing_private_key_in_pkcs8.pem -pubout > signing_public.pub

        These keys are used for signing. Hence, the creator of the JWT only publishes his public key for
        validation of the JWT that he signs with his private key.
     */

    // 2. Create JWT and sign it

    String filepathSigningPrivateKey = "src/test/resources/signedAndEncrypted/signing_private_key_in_pkcs8.pem";
    String filepathSigningPublicKey = "src/test/resources/signedAndEncrypted/signing_public.pub";

    RSAPrivateKey signingPrivateKey = (RSAPrivateKey) PemUtils.readPrivateKeyFromFile(filepathSigningPrivateKey, "RSA");
    RSAKeyProvider provider = mock(RSAKeyProvider.class);
    when(provider.getPrivateKeyId()).thenReturn("my-key-id");
    when(provider.getPrivateKey()).thenReturn(signingPrivateKey);
    when(provider.getPublicKeyById("my-key-id")).thenReturn(
            (RSAPublicKey) PemUtils.readPublicKeyFromFile(filepathSigningPublicKey, "RSA"));

    Algorithm algorithm = Algorithm.RSA256(provider);

    String signedToken = JWT.create()
            .withIssuer("auth0")
            .withClaim("name", "Bob")
            .sign(algorithm);

    assertEquals("eyJraWQiOiJteS1rZXktaWQiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9." +
                    "eyJpc3MiOiJhdXRoMCIsIm5hbWUiOiJCb2IifQ." +
                    "XzRwsAUP2fscy6jYGk5tVGwnjTCA8pyCpsHYZayh4qRfdMbJ6fBasvg0yqx8QPjSnJDCzYoYaFPw5of-33G4dQ",
            signedToken);

    /*
        3. Creating keys for encryption:

        These keys are created by the receiver of the JWT. The sender uses the public key of the receiver to
        encrypt the JWT, so that only the receiver can decrypt it.

        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

        // Create RSA-key in PKCS1 format (header "-----BEGIN RSA PRIVATE KEY-----")
        openssl genrsa -out encrypt_private_key_in_pkcs1.pem 2048

        // Convert to PKCS8 format (header "-----BEGIN PRIVATE KEY-----")
        openssl pkcs8 -topk8 -in encrypt_private_key_in_pkcs1.pem -outform pem -nocrypt -out encrypt_private_key_in_pkcs8.pem

        // Extract public key:
        openssl rsa -in encrypt_private_key_in_pkcs8.pem -pubout > encrypt_public.pub

        These keys are used for signing. Hence, the creator of the JWT only publishes his public key and keeps
        the secret key hidden.
     */


    // 4. Encrypt signed JWT (implementation from https://www.baeldung.com/java-rsa):

    String filepathEncryptPrivateKey = "src/test/resources/signedAndEncrypted/encrypt_private_key_in_pkcs8.pem";
    String filepathEncryptPublicKey = "src/test/resources/signedAndEncrypted/encrypt_public.pub";

    RSAPrivateKey encryptPrivateKey = (RSAPrivateKey) PemUtils.readPrivateKeyFromFile(
            filepathEncryptPrivateKey, "RSA");
    RSAPublicKey encryptPublicKey = (RSAPublicKey) PemUtils.readPublicKeyFromFile(
            filepathEncryptPublicKey, "RSA");

    Cipher encryptCipher = Cipher.getInstance("RSA");
    encryptCipher.init(Cipher.ENCRYPT_MODE, encryptPublicKey);

    byte[] secretMessageBytes = signedToken.getBytes(StandardCharsets.UTF_8);
    byte[] encryptedMessageBytes = encryptCipher.doFinal(secretMessageBytes);


    // 5. Decrypt signed JWT

    Cipher decryptCipher = Cipher.getInstance("RSA");
    decryptCipher.init(Cipher.DECRYPT_MODE, encryptPrivateKey);

    byte[] decryptedMessageBytes = decryptCipher.doFinal(encryptedMessageBytes);
    String decryptedSignedToken = new String(decryptedMessageBytes, StandardCharsets.UTF_8);

    assertEquals(signedToken, decryptedSignedToken);


    // 6. Verify JWT

    JWTVerifier verifier = JWT.require(algorithm)
            .withIssuer("auth0")
            .withClaim("name", "Bob")
            .build();

    verifier.verify(decryptedSignedToken);
}

Further Reading

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

This here is a great article about JWT best practices.