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 :
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.
@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 .