On this page
Intro
In this tutorial we will show 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 back end 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 back end 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:
./create-keys.sh
This will result in the following two main files being generated for testing:
File | Desccription |
---|---|
client.pub | A public key that will be deployed to the Identity Server and used to encrypt ID tokens |
client.p12 | A PKCS12 file that will be loaded into a keystore within the website, then used 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 file called client.pub
via the facilities menu in the top right of the Admin UI and select the asymmetric key option when prompted. No password is needed since this is only a public key:
Next navigate to Profiles / Token Service / General
and enable the use of ID token encryption, then select the following as whitelisted algorithms:
Then create a standard web client that uses the code flow with a client secret, and also activate encrypted ID tokens under the OpenID Connect settings:
Set the following OpenID Connect settings and note that the Spring Boot app will receive responses on these URLs, then use claims from the profile
scope to present the user name.
Field | Value |
---|---|
Redirect URI | http://localhost:8080/login/oauth2/code/curity |
Post Logout Redirect URI | http://localhost:8080/ |
Scope | openid profile |
The full XML for the web client is shown below:
<client><id>website-client</id><client-name>website-client</client-name><secret>$5$pjVbpI4Age8TuiGl$xu5MNyoiG4ZdYqgx03WFgdoaEIgs9mmX6M2HP2KhB.A</secret><redirect-uris>http://localhost:8080/login/oauth2/code/curity</redirect-uris><scope>openid</scope><scope>profile</scope><user-authentication><allowed-authenticators>Username-Password</allowed-authenticators><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>Client_Encryption_Public_Key</encryption-key><content-encryption-algorithm>A256CBC-HS512</content-encryption-algorithm><key-management-algorithm>RSA-OAEP</key-management-algorithm></id-token-encryption></client>
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:
spring:security:oauth2:client:registration:idsvr:client-id: website-clientclient-secret: Password1authorization-grant-type: authorization_coderedirect-uri: "{baseUrl}/login/oauth2/code/curity"scope: openid, profileprovider: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:
mvn packagejava -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:
After authentication the app displays the user's name by rendering claims received in the ID token, then shows a logout link:
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:
@Configurationclass 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 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:
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:
class JweDecryptor {fun decrypt(encryptedJwt: String): String {val privateKey = loadPrivateKey()val jwe = JsonWebEncryption()jwe.key = privateKeyjwe.compactSerialization = encryptedJwtreturn 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:
@Controllerclass 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 if there are reasons for keeping Personally Identifiable Information confidential. This may not always work automatically with the web client's chosen technology stack, though these can be customized via a JOSE compliant library that supports JSON Web Encryption. The web app can then continue to use ID token information in the standard way.