Information Security 8 min read

How to Secure OAuth2 Authorization Code Flow with PKCE in Spring Boot 3

This article explains the differences between confidential and public OAuth2 clients, illustrates the authorization‑code interception risk, and provides a step‑by‑step Spring Boot 3 implementation of PKCE—including configuration, code verifier generation, and token exchange—to harden security.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
How to Secure OAuth2 Authorization Code Flow with PKCE in Spring Boot 3

Environment: Spring Boot 3.1.0 with Spring Security OAuth2 Authorization Server 1.1.0.

1. Introduction

OAuth defines two client types based on their ability to keep credentials confidential:

Confidential client: Can protect its credentials (e.g., a server‑side application).

Public client: Cannot keep credentials secret (e.g., native mobile apps or browser‑based apps).

Public clients are vulnerable to authorization‑code interception attacks, where an attacker captures the code returned by the authorization endpoint and exchanges it for an access token.

Step (1): A native app on a device initiates an OAuth2 authorization request using a custom URI scheme.

Step (2): The request is forwarded to the authorization server over TLS.

Step (3): The server returns an authorization code.

Step (4): The code is sent back to the app via the custom redirect URI.

Note: Malicious apps can register the same custom URI scheme, intercepting the code in step (4) and later exchanging it for a token.

Mitigation with PKCE

PKCE (Proof Key for Code Exchange) introduces a dynamically generated code_verifier and its hashed counterpart code_challenge . The verifier is sent to the token endpoint and compared with the challenge stored by the server, preventing intercepted codes from being exchanged without the verifier.

2.2 Practical Example

Client Configuration (Spring Bean)

<code>@Bean
public RegisteredClientRepository registeredClientRepository() {
    RegisteredClient packClient = RegisteredClient
        .withId("pack001")
        .clientId("123123")
        .clientSecret("{noop}666666")
        .tokenSettings(TokenSettings.builder()
            .authorizationCodeTimeToLive(Duration.ofHours(2))
            .accessTokenTimeToLive(Duration.ofHours(2))
            .build())
        .clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
        .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
        .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
        .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
        .redirectUri("http://localhost:8080/index.html")
        .clientSettings(ClientSettings.builder()
            .requireAuthorizationConsent(true)
            .requireProofKey(true) // enable PKCE
            .build())
        .scope("openid")
        .build();
    return new InMemoryRegisteredClientRepository(packClient);
}
</code>

Generating a SHA‑256 code_verifier digest

<code>MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest("123".getBytes(StandardCharsets.US_ASCII));
String encodedVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
System.out.println(encodedVerifier);
</code>

The code_verifier must be a high‑entropy string (43‑128 characters) using unreserved characters per RFC 3986.

Authorization Request

http://localhost:9000/oauth2/v1/authorize?client_id=123123&response_type=code&redirect_uri=http://localhost:8080/index.html&code_challenge=pmWkWSBCL51Bfkhn79xPuKBKHz__H6B-mY6G9_eieuM&code_challenge_method=S256 The code_challenge value is derived from the code_verifier .

Token Exchange

oauth2/v1/token?grant_type=authorization_code&code=MXGyT1mtkJeXsw39oTiXh8VTnE0Pl--iiz0zojB3Mx8I3vuiEP59EcHkILPjsM5QK6X05W1V1jCaotu7_Gvyk5R-5BaPJcGWU1TE4YBRPH1nXFHADIfYRO0b4VGtnYO2&redirect_uri=http://localhost:8080/index.html&client_id=123123&client_secret=666666&code_verifier=123 The request includes the plain code_verifier , allowing the server to validate the exchange.

The response contains an access token (no refresh token in this example).

Conclusion

Combining OAuth2 authorization‑code flow with PKCE provides a robust security solution that mitigates code‑interception attacks by requiring a one‑time code_verifier known only to the legitimate client. Proper configuration in Spring Boot ensures that public clients remain secure without exposing client secrets.

Done!

Spring BootsecurityOAuth2authorization-serverPKCE
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.