JSON Web Token (JWT) with RSA encryption

RSA is a popular algorithm for asymmetric (public key) encryption that was established more than 40 years ago. Encrypting a JWT for a given recipient requires their public RSA key. The decryption takes place with the corresponding private RSA key, which the recipient must keep secret at all times.

To create an RSA encrypter with Nimbus JOSE+JWT for a given public key:

JWEEncrypter encrypter = new RSAEncrypter(rsaPublicKey);

To create an RSA decrypter:

JWEDecrypter decrypter = new RSADecrypter(rsaPrivateKey);

An absolutely essential security aspect in public key encryption is ensuring the data is encrypted for the intended recipient and not some other party, which will compromise the data's confidentiality. One solution is public key infrastructure, such as based on the PKIX / X.509 standard (used for SSL/TLS on the Internet and in other places).

The actual public key encryption is a two step process, to work around an RSA limitation that makes it unfeasible to encrypt data that is more than a few hundred bytes long. The library takes care of this internally, so you only need to supply the public key, the RSA algorithm name (alg) and the content encryption algorithm name (enc) in order to encrypt a piece of data, such as the claims for a JWT.

If you are curious know what those two encryption steps are:

  1. A single use secret AES or ChaCha20 key (called Content Encryption Key, or CEK) is generated to perform symmetrical encryption on the JWT payload. These symmetric ciphers are super efficient and can process plain text of (almost) arbitrary size. The type and length of the CEK to be generated is determined by the JWE "enc" header parameter. For example, A128GCM will cause a 128-bit AES CEK to be generated.
  2. The generated CEK, which fits the RSA size limitation, is then encrypted with RSA according to the JWE "alg" header parameter, and gets included as part of the JWT.

There are two standard RSA algorithm types for JSON Web Encryption (JWE), identified by the JWE "alg" header parameter:

JWE alg Description
The CEK is encrypted with RSAES with Optimal Asymmetric Encryption Padding (OAEP). Use RSA-OAEP-256 or another SHA-2 based RSA algorithm. Don't use RSA-OAEP because it's SHA-1 hashing is considered weak for today's applications.
The CEK is encrypted with RSAES-PKCS1-v1_5. Use of this algorithm is generally not recommended due to a security vulnerability. Provided for backward compatibility with older software.

You can combine a JWE "alg" with any of the following content encryption algorithms, identified by the JWE "enc" header parameter:

JWE alg Description
AES/CBC/HMAC/SHA-2 authenticated encryption
AES/GCM
eXtended-nonce ChaCha / Poly1305

The following example demonstrates RSA-OAEP-256 with A128GCM encryption of a JWT, where the recipient's java.security.interfaces.RSAPublicKey is used to encrypt the JWT. The recipient can then decrypt the JWT with its java.security.interfaces.RSAPrivateKey.

Check out /src/test/java/com/nimbusds/jwt/EncryptedJWTTest.java to see the
complete code.

import java.util.*;
import java.security.interfaces.*;
import javax.crypto.*;

import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.*;
import com.nimbusds.jwt.*;

// Compose the JWT claims set
Date now = new Date();

JWTClaimsSet jwtClaims = new JWTClaimsSet.Builder()
    .issuer("https://openid.net")
    .subject("alice")
    .audience(Arrays.asList("https://app-one.com", "https://app-two.com"))
    .expirationTime(new Date(now.getTime() + 1000*60*10)) // expires in 10 minutes
    .notBeforeTime(now)
    .issueTime(now)
    .jwtID(UUID.randomUUID().toString())
    .build();

System.out.println(jwtClaims.toJSONObject());
// Produces
// {
//   "iss" : "https://openid.net",
//   "sub" : "alice",
//   "aud" : [ "https://app-one.com" , "https://app-two.com" ],
//   "exp" : 1364293137871,
//   "nbf" : 1364292537871,
//   "iat" : 1364292537871,
//   "jti" : "165a7bab-de06-4695-a2dd-9d8d6b40e443"
// }

// Request JWT encrypted with RSA-OAEP-256 and 128-bit AES/GCM
JWEHeader header = new JWEHeader(
    JWEAlgorithm.RSA_OAEP_256,
    EncryptionMethod.A128GCM
);

// Create the encrypted JWT object
EncryptedJWT jwt = new EncryptedJWT(header, jwtClaims);

// Create an encrypter with the specified public RSA key
RSAEncrypter encrypter = new RSAEncrypter(publicKey);

// Do the actual encryption
jwt.encrypt(encrypter);

// Serialise to JWT compact form
String jwtString = jwt.serialize();

System.out.println(jwtString);
// Produces
//
// eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.K52jFwAQJH-
// DxMhtaq7sg5tMuot_mT5dm1DR_01wj6ZUQQhJFO02vPI44W5nDjC5C_v4p
// W1UiJa3cwb5y2Rd9kSvb0ZxAqGX9c4Z4zouRU57729ML3V05UArUhck9Zv
// ssfkDW1VclingL8LfagRUs2z95UkwhiZyaKpmrgqpKX8azQFGNLBvEjXnx
// -xoDFZIYwHOno290HOpig3aUsDxhsioweiXbeLXxLeRsivaLwUWRUZfHRC
// _HGAo8KSF4gQZmeJtRgai5mz6qgbVkg7jPQyZFtM5_ul0UKHE2y0AtWm8I
// zDE_rbAV14OCRZJ6n38X5urVFFE5sdphdGsNlA.gjI_RIFWZXJwaO9R.oa
// E5a-z0N1MW9FBkhKeKeFa5e7hxVXOuANZsNmBYYT8G_xlXkMD0nz4fIaGt
// uWd3t9Xp-kufvvfD-xOnAs2SBX_Y1kYGPto4mibBjIrXQEjDsKyKwndxzr
// utN9csmFwqWhx1sLHMpJkgsnfLTi9yWBPKH5Krx23IhoDGoSfqOquuhxn0
// y0WkuqH1R3z-fluUs6sxx9qx6NFVS1NRQ-LVn9sWT5yx8m9AQ_ng8MBWz2
// BfBTV0tjliV74ogNDikNXTAkD9rsWFV0IX4IpA.sOLijuVySaKI-FYUaBy
// wpg

// Parse
jwt = EncryptedJWT.parse(jwtString);

// Create a decrypter with the specified private RSA key
RSADecrypter decrypter = new RSADecrypter(privateKey);

// Decrypt
jwt.decrypt(decrypter);

// Retrieve JWT claims
System.out.println(jwt.getJWTClaimsSet().getIssuer());;
System.out.println(jwt.getJWTClaimsSet().getSubject());
System.out.println(jwt.getJWTClaimsSet().getAudience().size());
System.out.println(jwt.getJWTClaimsSet().getExpirationTime());
System.out.println(jwt.getJWTClaimsSet().getNotBeforeTime());
System.out.println(jwt.getJWTClaimsSet().getIssueTime());
System.out.println(jwt.getJWTClaimsSet().getJWTID());