Proof Key for Code Exchange (PKCE) has become a standard security enhancement to the OAuth 2.0 Authorization Code Flow—especially in public clients like mobile and single-page applications. But PKCE isn’t just for frontend apps. When combined with a stateless backend built with Kotlin and Spring Boot, it strengthens your security posture, particularly when you’re avoiding client secrets.
This guide walks you through how to implement a secure PKCE flow using Kotlin and Spring Boot, including endpoint structure, code challenge generation, and token exchange.
Why Use PKCE with Spring Boot?
PKCE was designed to protect public clients from authorization code interception attacks. But it also benefits backend applications that:
- Don’t want to manage confidential client secrets.
- Support both browser and native app clients.
- Need flexible OAuth 2.0 integrations with high security.
While Spring Security supports OAuth 2.0 out of the box, PKCE is not always enabled by default in backend flows, and it requires custom configuration.
PKCE Flow Recap
The PKCE-enhanced flow works as follows:
- Client generates a
code_verifier
and a hashedcode_challenge
. - It starts the OAuth 2.0 authorization request with the
code_challenge
. - The user authenticates and the authorization server returns a code.
- The client exchanges the code for a token using the
code_verifier
.
+--------+ +---------------+
| |--(A)- Authorization Request ------>| |
| | code_challenge (+ method) | |
| | | Authorization |
| Client |<-(B)---- Authorization Code -------| Server |
| | | |
| |--(C)-- Token Request -------------->| |
| | code_verifier | |
| |<-(D)----- Access Token -------------| |
+--------+ +---------------+
Step 1: Generate Code Verifier and Code Challenge
You can generate the code verifier and challenge in Kotlin like this:
// Kotlin snippet to generate code_verifier and code_challenge
val secureRandom = SecureRandom()
val codeVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(
ByteArray(32).apply { secureRandom.nextBytes(this) }
)
val digest = MessageDigest.getInstance("SHA-256")
val hashed = digest.digest(codeVerifier.toByteArray(StandardCharsets.US_ASCII))
val codeChallenge = Base64.getUrlEncoder().withoutPadding().encodeToString(hashed)
println("Code Verifier: $codeVerifier")
println("Code Challenge: $codeChallenge")
Use the code_challenge
in your authorization URL.
Step 2: Build Authorization URL with PKCE
// Kotlin snippet to build authorization URL
val authorizationUrl = UriComponentsBuilder
.fromUriString("https://your-oauth-server.com/oauth2/authorize")
.queryParam("response_type", "code")
.queryParam("client_id", "your-client-id")
.queryParam("redirect_uri", "http://localhost:8080/callback")
.queryParam("scope", "openid profile email")
.queryParam("code_challenge", codeChallenge)
.queryParam("code_challenge_method", "S256")
.build()
.toUriString()
This URL redirects the user to the OAuth authorization server, where they will login and authorize the app.
Step 3: Exchange Authorization Code for Token
Once you receive the authorization code
, exchange it for tokens:
// Spring WebClient example with PKCE token request
val response = webClient.post()
.uri("https://your-oauth-server.com/oauth2/token")
.body(BodyInserters.fromFormData("grant_type", "authorization_code")
.with("client_id", "your-client-id")
.with("redirect_uri", "http://localhost:8080/callback")
.with("code", receivedCode)
.with("code_verifier", codeVerifier))
.retrieve()
.bodyToMono(TokenResponse::class.java)
.block()
Define TokenResponse
as a data class for mapping token results.
Real-World Considerations
- Rotate
code_verifier
per request; never reuse. - Use HTTPS for all token and authorization exchanges.
- PKCE flow complements rather than replaces your need for secure storage (especially refresh tokens).
Use with ForgeRock or Identity Providers
ForgeRock Identity Cloud supports PKCE natively. You can easily enable it in your client configuration. 👉 Related: Understanding the Authorization Code Flow with PKCE in OAuth 2.0
👉 Related: Building Complete OIDC Login Flow URLs in ForgeRock Identity Cloud
Conclusion
Using PKCE with Kotlin and Spring Boot isn’t just about mobile support—it’s a general OAuth 2.0 best practice. It protects your applications from malicious code interception while maintaining high developer agility.
Whether you’re working on a microservice backend, BFF layer, or hybrid app, PKCE is a lightweight security upgrade worth adopting.
🔍 What’s Next?
- Can you combine PKCE with Refresh Token Rotation securely?
- How would you handle PKCE flow in a multi-tenant SaaS app?
- Should confidential clients also implement PKCE?
Let’s explore more in the next post.