/images/resources/tutorials/writing-clients/tutorials-encrypted-id-tokens.jpg

OpenID Connect Client with Encrypted ID Tokens

Advanced Security
QualityAvailability
Download on GitHub
On this page

Intro

This tutorial shows how to use the Curity Identity Server and JSON Web Encryption (JWE), to protect ID tokens that contain Personally Identifiable Information (PII). The web app must be a confidential client, where a backend component manages the private key used for decryption. See the Encrypted ID Tokens article for further details on how encrypted ID tokens are used.

The code example provides a Spring Boot website developed with Kotlin, though the same techniques could be applied to any other backend technology. The same solution is possible for a single page application (SPA) that uses a backend for frontend (BFF) approach, such as the Token Handler Pattern.

Create Encryption Keys

First, get the code by cloning the GitHub repository, then run the supplied helper script, which will create public and private keys that are used for encryption:

bash
1
./create-keys.sh

This will result in the following two main files being generated for testing:

FileDescription
client.pubThe public key used by the Curity Identity Server to encrypt ID tokens
client.p12A password protected file containing the public key, and also the private key used by the website to decrypt ID tokens

Note

Curity Identity Server is used in this example, but other OAuth servers can also be used.

Configure the Curity Identity Server

Import the client’s public key via the Facilities menu in the top right of the Admin UI. Navigate to Keys and Cryptography -> Crypto. Click on the + next to Encryption Keys and select the asymmetric key type from the drop-down, then enter a name of website-encryption. Click the Upload Existing button, then select File. Browse to either the client.pub or client.p12 file and then click Add and Commit. If uploading the P12 file, also enter its password, which is available in the create-keys.sh script:

Encryption Public Key

Navigate to Profiles -> Token Service -> General and enable the use of ID token encryption, then select the following options as whitelisted algorithms:

Encrypted ID Tokens Enabled

Next, you need a standard web client that uses the code flow with a client secret, with encrypted ID tokens activated. The correct settings configured in the Admin UI are shown here:

Client ID Tokens Enabled

The following OpenID Connect settings can be used with the code example. The Spring Boot app will receive responses on these URLs, then use claims from the profile scope to present the username:

FieldValue
Redirect URIhttp://localhost:8080/login/oauth2/code/curity
Post Logout Redirect URIhttp://localhost:8080/
Scopeopenid profile

The full XML for the web client is provided below. You can save it to a file and then upload the configuration in the Admin UI, to create the client. Do so by accessing the Changes menu and selecting the Upload option.

xml
1234567891011121314151617181920212223242526272829303132333435363738394041
<config xmlns="http://tail-f.com/ns/config/1.0">
<profiles xmlns="https://curity.se/ns/conf/base">
<profile>
<id>token-service</id>
<type xmlns:as="https://curity.se/ns/conf/profile/oauth">as:oauth-service</type>
<settings>
<authorization-server xmlns="https://curity.se/ns/conf/profile/oauth">
<client-store>
<config-backed>
<client>
<id>website-client</id>
<client-name>website-client</client-name>
<secret>Password1</secret> <!-- Don't forget to change this -->
<redirect-uris>http://localhost:8080/login/oauth2/code/curity</redirect-uris>
<scope>openid</scope>
<scope>profile</scope>
<user-authentication>
<allowed-post-logout-redirect-uris>http://localhost:8080/</allowed-post-logout-redirect-uris>
</user-authentication>
<allowed-origins>http://localhost:8080</allowed-origins>
<capabilities>
<code>
</code>
</capabilities>
<use-pairwise-subject-identifiers>
<sector-identifier>website-client</sector-identifier>
</use-pairwise-subject-identifiers>
<validate-port-on-loopback-interfaces>true</validate-port-on-loopback-interfaces>
<id-token-encryption>
<encryption-key>website-encryption</encryption-key>
<content-encryption-algorithm>A256CBC-HS512</content-encryption-algorithm>
<key-management-algorithm>RSA-OAEP</key-management-algorithm>
</id-token-encryption>
</client>
</config-backed>
</client-store>
</authorization-server>
</settings>
</profile>
</profiles>
</config>

Build the Code

The code example is based on the OpenID Connect Client with Spring Security, and implements the Code Flow. Open the project in an IDE of your choice, then ensure that the website's OAuth settings match those of your environment:

yaml
1234567891011121314
spring:
security:
oauth2:
client:
registration:
idsvr:
client-id: website-client
client-secret: Password1
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/curity"
scope: openid, profile
provider:
idsvr:
issuer-uri: https://localhost:8443/oauth/v2/oauth-anonymous

Ensure that Java 8 or later and Maven are installed, then run the following commands to build and run the app, which will listen on port 8080 by default:

bash
12
mvn package
java -jar target/example-website-0.0.1-SNAPSHOT.jar

Run the Web Client

The website application has a minimal UI, so that the focus is only on working with encrypted ID tokens. Browse to http://localhost:8080, which is an unsecured home page that prompts the user to sign in:

Unauthenticated View

After authentication the app displays the user's name by rendering claims received in the ID token, then shows a logout link:

Authenticated View

OAuth Customization

Some OAuth security libraries used by web clients do not yet support JSON Web Encryption. At the time of writing this includes Spring Boot, but we can use Spring's extensibility features to customize handling of responses from the token endpoint. This results in the following web security configuration, where a custom handler is used to process responses from the token endpoint:

kotlin
123456789101112131415161718
@Configuration
class OAuth2SecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/error").permitAll()
.anyRequest().authenticated()
.and()
.logout()
.logoutSuccessHandler(CustomLogoutHandler())
.and()
.oauth2Login()
.tokenEndpoint()
.accessTokenResponseClient(accessTokenResponseClient())
}
}

A utility Token Response Converter then manages updating the token response with a Signed JWT that is extracted from the ciphertext of the Nested JWT. A little boiler plate code is needed, not all of which is shown in the below code snippet. The Spring Security processing then continues in the standard way, to digitally verify the signed JWT:

kotlin
1234567891011121314151617181920
class CustomTokenResponseConverter : Converter<Map<String?, String?>?, OAuth2AccessTokenResponse?> {
override fun convert(tokenResponseParameters: Map<String?, String?>): OAuth2AccessTokenResponse {
var idToken = tokenResponseParameters["id_token"]
if (idToken != null) {
idToken = JweDecryptor().decrypt(idToken);
}
val extraParams = mapOf("id_token" to idToken)
return OAuth2AccessTokenResponse
.withToken(accessToken)
.refreshToken(refreshToken)
.scopes(scopes)
.tokenType(accessTokenType)
.expiresIn(expiresIn)
.additionalParameters(extraParams)
.build()
}
}

To manage the JSON Web Encryption the code example first gets a private key object by loading a password protected PKCS12 file into a keystore. The jose4j security library is then used to perform the JWE decryption, which only requires a few lines of code:

kotlin
1234567891011
class JweDecryptor {
fun decrypt(encryptedJwt: String): String {
val privateKey = loadPrivateKey()
val jwe = JsonWebEncryption()
jwe.key = privateKey
jwe.compactSerialization = encryptedJwt
return jwe.plaintextString
}
}

From this point onwards the application works with an ID token that is the inner JWT of the nested JWT. The example demonstrates this by rendering the given_name and family_name claims from the ID token:

kotlin
12345678910111213141516171819
@Controller
class UserController {
@GetMapping("/")
fun index(): String {
return "index"
}
@GetMapping("/user")
fun user(
model: Model,
@AuthenticationPrincipal oidcUser: OidcUser
): String {
model.addAttribute("userName",
"${oidcUser.idToken.givenName} ${oidcUser.idToken.familyName}")
return "user"
}
}

Logout

The website also provides a basic OpenID Connect logout capability, but is careful not to send the decrypted ID token in the front channel and reveal PII. This is done by implementing a custom logout handler that omits the id_token_hint field mentioned in OpenID Connect RP Initiated Logout.

Conclusion

Web clients can use encrypted ID tokens to prevent personally identifiable information being revealed to the browser. This may not always work automatically with the web client's technology stack, though you can customize behavior using a JOSE compliant library that supports JSON Web Encryption. The web app can then continue to use ID token information in the standard way.

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