Using JSON web encryption to protect the confidentiality of ID tokens

Encrypted ID Tokens

On this page

Intro

Confidential client applications can use JSON Web Encryption (JWE) to protect the confidentiality of ID tokens, which use the JWT format. This is typically done by configuring an asymmetric public key in the Authorization Server, which is then used to encrypt the JWT at the time of token issuance. The client application's back end can then use the corresponding private key to decrypt the ID token:

Diagram showing a web application, which can either use a website or a token handler to store the private key and decrypt the ID token

This example shows a web application, which can either use a Website or a Token Handler to store the private key and decrypt the ID token. The app's browser code cannot read the encrypted ID token, so would typically get the information contained in it via a call to the back end with a secure cookie.

Security Regulations

In some sectors, regulations such as those in Financial-grade API (FAPI) Profiles may need to be met. Therefore, this article will explain how encrypted ID tokens work.

  • The FAPI 1.0 baseline profile recommends that any ID tokens received on the front channel are encrypted, such as when using the Hybrid Flow. This prevents any Personally Identifiable Information (PII) from being revealed to the browser or written to server logs.

FAPI 2.0

ID token encryption is not needed in FAPI 2.0 anymore because it does not support sending the ID token in the front channel. Read more about FAPI in What is Financial-Grade?

Encrypted Access Tokens?

It does not usually make sense to encrypt access tokens, since doing so would not prevent an attacker from sending one to an API. The confidentiality of access tokens is instead ensured by returning them to clients in an opaque unreadable format, as described in the Phantom Token Pattern.

Further protection is possible via Sender Constrained Access Tokens, which ensures that an attacker who somehow steals an access token cannot successfully call an API with it.

JSON Web Encryption

There are two possible formats for a JWT, the first of which is widely known and consists of three sections separated by the dot character, for the header, payload and signature.

The second format is used when the payload needs to be kept confidential, and JSON Web Encryption (JWE) is then used, as detailed in RFC7516. The Authorization Server then uses the client's public key to encrypt a separate symmetric encryption key and return it to the client. The JWT then has five sections that are listed below:

  • Header
  • Content encryption key (CEK)
  • Initialization vector
  • Ciphertext / payload
  • Authentication tag

Example header data is shown below, where the client first uses its private key to decrypt the CEK, using the algorithm specified in the alg field. The resulting symmetric key is then used to decrypt the ciphertext / payload, using the algorithm in the enc header field, to get the JWT in plaintext.

json
123456
{
"alg": "RSA-OAEP",
"enc": "A256CBC-HS512",
"cty": "JWT",
"x5t#S256": "Y0H4y9eaiiNyoprMY6vMm4i3LATu0LEBXgSv7F1iNlU"
}

The cty field indicates that the content type is a JWT, so that the encrypted ID token is actually a Nested JWT. In scenarios where a client has multiple possible encryption keys, additional header fields, such as an x5t#S256 thumbprint, can inform the client which key should be used to decrypt the CEK.

Decryption in Clients

Some technology stacks will not be able to process the ID token if it is encrypted, and a library with JWE decryption capabilities is needed. Some examples are provided on the OpenID Developers JWT website. The following code snippet shows some decryption code using the JOSE Library for Node.js:

javascript
123456789101112131415161718192021
import crypto from 'crypto';
import fs from 'fs'
import {compactDecrypt} from 'jose/jwe/compact/decrypt'
(async () => {
const buffer = fs.readFileSync('../keys/private.key');
const pem = buffer.toString();
const privateKey = crypto.createPrivateKey(pem);
const jweEncryptedJwt = 'eyJraWQiOiItOTk2NzcwMjc0IiwieD ...';
const options = {
keyManagementAlgorithms: ['RSA-OAEP'],
contentEncryptionAlgorithms: ['A256CBC-HS512'],
};
const {plaintext: decryptedData, protectedHeader: jwtHeader} = await compactDecrypt(jwe, privateKey, options);
const jwtPayload = new TextDecoder().decode(decryptedData);
console.log(jwtHeader);
console.log(jwtPayload);
})();

Once decrypted, the plaintext is a standard digitally signed JWT containing the fields the app has been configured to receive in ID tokens:

json
1234567891011121314151617181920212223242526
{
"kid": "38074812",
"alg": "RS256"
},
{
"exp": 1632828702,
"nbf": 1632825102,
"jti": "d4f31129-fee4-472e-8bca-c338599f06b0",
"iss": "https://login.example.com/oauth/v2/oauth-anonymous",
"aud": "website-client",
"sub": "dcc19309f4164aea1ed56e7c61f3bfb949d672449f09e3029e9378d7ab6154c4",
"auth_time": 1632822775,
"iat": 1632825102,
"purpose": "id",
"at_hash": "hSJT7fjWKnf4y44Bc0ctmg",
"amr": "urn:se:curity:authentication:html-form:Username-Password"
"acr": "urn:se:curity:authentication:html-form:Username-Password",
"delegation_id": "0b930571-a645-4ffe-b937-83f82a21819f",
"nonce": "gIbR_arYFRZ-vCq6_8yzQjN1fjSbt9DHdz4O097R76A",
"s_hash": "MqTQ2ilHJuG5YN2QKx516w",
"azp": "website-client",
"sid": "updzF6Hcmy123PbC",
"preferred_username": "john.doe",
"given_name": "John",
"family_name": "Doe"
}

The client can then continue by verifying the JWT signature to ensure its integrity, then using its payload fields within the application in the standard way. This may include rendering the user's name from the given_name and family_name claims.

Choosing Algorithms

The algorithms used to encrypt and decrypt JWTs are specified in JSON Web Algorithms (JWA) and this is covered in the RFC7518 document. The following links discuss the algorithms that can be used, and provides recommendations based on the security level.

Algorithm TypeExample Algorithm
Key ManagementRSA-OAEP
Content EncryptionA256CBC-HS512

FAPI may also provide recommendations on JWS algorithms, and point out any that are potentially weak and should not be used, such as RSA1_5. The values in the above table are secure options and suitable for use in financial-grade solutions.

Metadata

Authorization Servers that support encrypted ID tokens indicate this in their OpenID Connect Provider Metadata, at the /.well-known/openid-configuration endpoint:

Metadata FieldMeaning
id_token_encryption_alg_values_supportedAlgorithms the client can use to deciher the encryption key of encrypted ID tokens
id_token_encryption_enc_values_supportedAlgorithms the client can use to decipher the payload of encrypted ID tokens

Client Configuration

To configure a static OAuth client to use encrypted JWTs, the process is to first import the encryption public key into the Authorization Server, Next the client will be configured to use that key in its ID token encryption, and the JWE algorithms will be selected from those supported.

Dynamic Clients

Dynamic clients activate encrypted ID tokens when they register, using the following fields defined in OpenID Connect Registration Client Metadata:

Metadata FieldMeaning
id_token_encrypted_response_algIf this is supplied, the response will be signed then encrypted, with the result being a Nested JWT
id_token_encrypted_response_encThe default value is A128CBC-HS256, though in some cases this can be overridden by the client
jwksDynamic clients typically send the encryption public key as a JSON web key set, or instead provide a URL value for downloading it, in the jwks_uri field

An example registration request to create a dynamic client that uses encrypted ID tokens is represented below. The client ID and client secret returned in the response could then be used to run a code flow, and after authentication an encrypted ID token would be returned.

json
12345678910111213141516
{
"redirect_uris":["https://oauth.tools/callback/code"],
"post_logout_redirect_uris":["https://oauth.tools"],
"application_type":"web",
"grant_types":["authorization_code"],
"scope":"openid",
"id_token_encrypted_response_alg": "RSA-OAEP",
"id_token_encrypted_response_enc": "A256CBC-HS512",
"jwks": {
"keys": [{
"kty": "RSA",
"n": "opiojI2q1f5AFjlIyegBRXb5hxL3itRtzJS69btiRRIdxFAilU9eJM86lWtFBsYHVndZS1RmmvAigdw4-6OAsR2lO01Lo4XUgBvaQC9MD50NlQMm-BN4m2NsrdR81skP7B0AnQSnOmKS7ZfoVgE3R3EOOR2b9MXfJqQ-SYJAxcttW7KSY-jyvnn_7UlTWS4pC5fH1uAgIp__SAdLWojZvW2RpuTH1pHPUrdk7uo7PQ8MX8a5Gb-LYr15cssg2qodqds_liKloCuJ9It3NC20XkkW2dwfkSYiGNsVa3ZNEkvImz4RDUiu0K6wvwYZuR4sQyCb7HFgAL7JW_IwyHxV8w",
"e": "AQAB"
}]
}
}

Logout

If the website needs a logout capability then care is needed to ensure that the unencrypted ID token is not sent on the front channel, such as the id_token_hint field used for OpenID Connect RP Initiated Logout. This may require the ability to implement a custom logout handler that excludes this field.

Conclusion

It is important to avoid revealing sensitive data such as Personally Identifiable Information when using ID tokens. One way to achieve this is to encrypt ID tokens using JSON Web Encryption.

Client applications will then receive an encrypted JWT and must use security libraries that support JWE decryption. To understand how to use encrypted ID tokens with the Curity Identity Server, see the Website using Encrypted ID Tokens code example.

Join our Newsletter

Get the latest on identity management, API Security and authentication straight to your inbox.

Start Free Trial

Try the Curity Identity Server for Free. Get up and running in 10 minutes.

Start Free Trial