Security Top

I just announced the new Learn Spring Security course, including the full material focused on the new OAuth2 stack in Spring Security 5:

>> CHECK OUT THE COURSE
Authors Top

If you have a few years of experience in the Java ecosystem, and you’d like to share that with the community, have a look at our Contribution Guidelines.

1. Introduction

In this tutorial, we'll show to use PKCE in a Spring Boot confidential client application.

2. Background

Proof Key for Code Exchange (PKCE) is an extension to the OAuth protocol that initially targeted public clients, usually SPA web applications or mobile apps. It is used as part of the Authorization Code Grant flow and helps to mitigate some attacks by a malicious third party.

The main vector for those attacks is the step that happens when the provider has already established the user's identity and sends the authorization code using an HTTP redirect. Depending on the scenario, this authorization code can leak and/or be intercepted, allowing the attacker to use it to obtain a valid access token.

Once in possession of this access token, the attacker can use it to access a protected resource and use it as if it was the legitimate owner. For example, if this access token is associated with a banking account, they can then access statements, portfolio values, or other sensitive information.

3. PKCE Modifications to OAuth

The PKCE mechanism adds a few tweaks to the standard authorization code flow:

  • The client sends two additional parameters in the initial authorization request: code_challenge and code_challenge_method
  • In the last step, when the client exchanges an authorization code for an access token, there’s also a new parameter: code_verifier

A PKCE-enabled client takes the following steps to implement this mechanism:

First, it generates a random string to use as the code_verifier parameter. According to RFC 7636, the length of this string must be at least 43 octets but less than 128 octets. The key point is to use a secure random generator, such as the JVM's SecureRandom or equivalent.

Besides its length, there's also a restriction on the range of allowed characters: only alphanumeric ASCII characters are supported, along with a few symbols.

Next, the client takes the generated value and transforms it into the code_challenge parameter using a supported method. Currently, the specification mentions just two transformation methods: plain and S256.

  • plain is just a no-op transformation, so the transformed value is the same as the code_verifier
  • S256 corresponds to the SHA-256 hashing algorithm, whose result is encoded in BASE64

The client then builds the OAuth authorization URL using the regular parameters (client_id, scope, state, etc.) and adds the resulting code_challenge and code_challenge_method.

3.1. Code Challenge Verification

In the last step of an OAuth authorization code flow, the client sends the original code_verifier value along with the regular ones as defined by this flow. The server then validates the code_verifier according to the challenge's method:

  • For the plain method, code_verifier and the challenge must be the same
  • For the S256 method, the server calculates the SHA-256 of the supplied value and encodes it in BASE64 before comparing it with the original challenge.

So, why is PKCE effective against authorization code attacks? As we mentioned before, those usually target the redirect sent from the authorization server, which contains the authorization code, to work. However, with PKCE, this information is no longer sufficient to complete the flow, at least for the S256 method. The code-for-token exchange only happens if the client provides both the authorization code and the verifier, which is never present in the redirects.

Of course, when using the plain method, the verifier and challenge are the same, so there's no point in using this method in real-world applications.

3.2. PKCE for Secret Clients

In OAuth 2.0, PKCE is optional and mostly used with mobile and web applications. The upcoming OAuth 2.1 version, however, made PKCE mandatory not only for public clients but also for secret ones.

Just to remember, a secret client is usually a hosted application running in a cloud or on-premises server. Such clients also use the authorization code flow, but since the final code exchange step happens between the backend and the authorization servers, the user agent (web or mobile) never “sees” the access token.

Other than that, the steps are exactly the same as in the public client case.

4. Spring Security Support for PKCE

As of Spring Security 5.7, PKCE is fully supported for both servlet and reactive flavored web applications. However, this feature is not enabled by default since not all identity providers support this extension yet. Spring Boot applications must use version 2.7 or above of the framework and rely on standard dependency management. This ensures the project picks the correct Spring Security version, along with its transitive dependencies.

PKCE support lives in the spring-security-oauth2-client module. For a Spring Boot application, the easiest way to bring this dependency is using the corresponding starter module:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    <version>2.7.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
    <version>2.7.2</version>
</dependency>

The latest versions of those dependencies can be downloaded from Maven Central.

With the dependencies in place, we now need to customize the OAuth 2.0 login process to support PKCE. For reactive applications, this means adding a SecurityWebFilterChain bean that applies this setting:

@Bean
public SecurityWebFilterChain pkceFilterChain(ServerHttpSecurity http,
  ServerOAuth2AuthorizationRequestResolver resolver) {
    http.authorizeExchange(r -> r.anyExchange().authenticated());
    http.oauth2Login(auth -> auth.authorizationRequestResolver(resolver));
    return http.build();
}

The key step is setting a custom ServerOAuth2AuthorizationRequestResolver in the login specification. Spring Security uses an implementation of this interface to build an OAuth authorization request for a given client registration.

Fortunately, we don't have to implement this interface. Instead, we can use the readily available DefaultServerOAuth2AuthorizationRequestResolver class, which allows us to apply further customizations:

@Bean
public ServerOAuth2AuthorizationRequestResolver pkceResolver(ReactiveClientRegistrationRepository repo) {
    var resolver = new DefaultServerOAuth2AuthorizationRequestResolver(repo);
    resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce());
    return resolver;
}

Here, we instantiate the request resolver, passing a ReactiveClientRegistrationRepository instance. Then, we use OAuth2AuthorizationRequestCustomizers.withPkce(), which provides the required logic to add the additional PKCE parameters to the authorization request URL.

5. Testing

To test our PKCE-enabled application, we need an authorization server that supports this extension. In this tutorial, we'll use the Spring Authorization Server for this purpose. This project is a recent addition to Spring's family that allows us to quickly build an OAuth 2.1/OIDC-compliant authorization server.

5.1. Authorization Server Setup

In our live test environment, the authorization server runs as a separate process from the client. The project is a standard Spring Boot web application to which we've added the relevant maven dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.7.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-authorization-server</artifactId>
    <version>0.3.1</version>
</dependency>

The latest version of the starter and  Spring Authorization Server can be downloaded from Maven Central.

To work properly, the Authorization Server requires us to provide a few configuration beans, including a RegisteredClientRepository and an UserDetailsService. For our testing purposes, we can use in-memory implementations of both containing a fixed set of test values. For this tutorial, the former is more relevant:

@Bean 
public RegisteredClientRepository registeredClientRepository() {      
    var pkceClient = RegisteredClient
      .withId(UUID.randomUUID().toString())
      .clientId("pkce-client")
      .clientSecret("{noop}obscura")
      .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
      .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
      .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
      .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
      .scope(OidcScopes.OPENID)          
      .scope(OidcScopes.EMAIL)          
      .scope(OidcScopes.PROFILE)
      .clientSettings(ClientSettings.builder()
        .requireAuthorizationConsent(false)
        .requireProofKey(true)
        .build())
      .redirectUri("http://127.0.0.1:8080/login/oauth2/code/pkce")
      .build();
    
    return new InMemoryRegisteredClientRepository(pkceClient);
}

The key point is using the clientSettings() method to enforce the use of PKCE for a particular client. We do this by passing a ClientSettings object created with the requireProofKey() set to true.

In our test setup, the client will run on the same host as the authorization server, so we're using 127.0.0.1 as the hostname part of the redirect URL. It is worth noting that using “localhost” is not allowed here, hence the use of the equivalent IP address.

To complete the setup, we'll also need to modify the default port setting in the application’s properties file:

server.port=8085

5.2. Running Live Tests

Now, let's run a live test to verify that all is working as intended. We can run both projects straight from the IDE or open two shell windows and issue the command mvn spring-boot:run for each module. Regardless of the method, once both applications are up, we can open a browser and point it to http://127.0.0.1:8080.

We should see Spring Security's default login page:

Notice the URL in the address bar: http://localhost:8085. This means that the login form came from the authorization server through a redirect. To verify this statement, we can open Chrome's DevTools (or the equivalent in your browser of choice) while on the login form and reenter the initial URL in the address bar:

We can see PKCE parameters in the Location header present in the response generated by our client application to the request made to http://127.0.0.1:8080/oauth2/authorization/pkce:

Location: http://localhost:8085/oauth2/authorize?
  response_type=code&
  client_id=pkce-client&
  scope=openid email&
  state=sUmww5GH14yatTwnv2V5Xs0rCCJ0vz0Sjyp4tK1tsdI=&
  redirect_uri=http://127.0.0.1:8080/login/oauth2/code/pkce&
  nonce=FVO5cA3_UNVVIjYnZ9ZrNq5xCTfDnlPERAvPCm0w0ek&
  code_challenge=g0bA5_PNDxy-bdf2t9H0ximVovLqMdbuTVxmGnXjdnQ&
  code_challenge_method=S256

To complete the login sequence, we'll use “user” and “password” as credentials. If we continue to follow the requests, we'll see that neither the code verifier nor the access token is ever present, which was our goal.

6. Conclusion

In this tutorial, we've shown how to enable OAuth's PKCE extension in a Spring Security application with just a few lines of code. Furthermore, we've also shown how to use the Spring Authorization Server library to create a tailor-made server for testing purposes.

As usual, all code is available over on GitHub.

Security bottom

I just announced the new Learn Spring Security course, including the full material focused on the new OAuth2 stack in Spring Security 5:

>> CHECK OUT THE COURSE
Security footer banner
guest
0 Comments
Inline Feedbacks
View all comments