Website with Encrypted ID Tokens

Website with Encrypted ID Tokens

Code Examples / website-integration

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 Back End for Front End (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:

FileDesccription
client.pubA public key that will be deployed to the Identity Server and used to encrypt ID tokens
client.p12A PKCS12 file that will be loaded into a keystore within the website, then used to decrypt ID tokens

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:

Encryption Public Key

Next navigate to Profiles / Token Service / General and enable the use of ID token encryption, then select the following as whitelisted algorithms:

Encrypted ID Tokens Enabled

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:

Client ID Tokens Enabled

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.

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

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:

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

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

Keep up with our latest articles and how-tos RSS feeds.