OAuth 2.0 & OIDC
Implementing the Authorization Code Flow with PKCE for Web Apps
Learn how to secure public and private clients using the authorization code flow enhanced with Proof Key for Code Exchange to prevent authorization code injection.
In this article
Demystifying Proof Key for Code Exchange
Proof Key for Code Exchange, commonly referred to as PKCE, was originally developed to secure mobile applications but is now the recommended standard for all web applications. It provides a way for a public client to prove its identity dynamically for every single login request. Instead of a shared static secret, PKCE uses a unique, one-time cryptographic challenge that links the initial authorization request to the final token exchange.
The beauty of this mechanism lies in its simplicity and its reliance on basic cryptographic principles. By creating a temporary secret on the fly, the application ensures that even if an attacker intercepts the authorization code, the code is useless without the original secret that generated the challenge. This effectively closes the loop on authorization code injection and interception attacks.
- Code Verifier: A high-entropy random string generated by the client for every request.
- Code Challenge: A transformed version of the verifier, usually hashed using SHA-256.
- Code Challenge Method: The algorithm used to transform the verifier, typically S256.
When the user begins the login process, the application generates the Code Verifier and stores it locally in the browser. It then sends the Code Challenge to the Identity Provider along with the standard authorization request parameters. The Identity Provider records this challenge and associates it with the specific authorization code it issues to the user.
The Cryptographic Handshake
After the user authenticates and the application receives the authorization code, it makes a POST request to the token endpoint to exchange the code for an access token. In this second step, the application includes the original, unhashed Code Verifier. The Identity Provider then applies the same hashing algorithm to the received verifier and compares the result to the challenge it received earlier.
If the calculated hash matches the stored challenge, the Identity Provider knows that the entity requesting the token is the exact same entity that initiated the login. If the hashes do not match, the request is immediately rejected, protecting the user from an intercepted code. This ensures that the security of the flow depends on the client possessing a secret that was never transmitted during the initial redirect.
1// Generate a random string for the code verifier
2function generateRandomString(length) {
3 const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
4 let result = '';
5 const values = new Uint32Array(length);
6 window.crypto.getRandomValues(values);
7 for (let i = 0; i < length; i++) {
8 result += charset[values[i] % charset.length];
9 }
10 return result;
11}
12
13// Hash the verifier using SHA-256 to create the challenge
14async function generateCodeChallenge(verifier) {
15 const encoder = new TextEncoder();
16 const data = encoder.encode(verifier);
17 const digest = await window.crypto.subtle.digest('SHA-256', data);
18 return btoa(String.fromCharCode(...new Uint8Array(digest)))
19 .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
20}Implementing PKCE in a React Single Page Application
In a real-world React application, managing the PKCE flow requires careful state handling across page redirects. When the user clicks the login button, the app must generate the verifier, calculate the challenge, and save the verifier in a secure location like sessionStorage. This is necessary because the browser will navigate away to the Identity Provider and then return to a different route in your application.
Using sessionStorage is generally preferred over localStorage for storing the Code Verifier because it is scoped to the specific browser tab and is cleared when the tab is closed. This minimizes the risk of the verifier persisting longer than necessary. Once the application is redirected back from the Identity Provider, it retrieves the verifier from storage to complete the exchange.
Modern libraries like MSAL, Auth0-SPA-JS, or oidc-client-ts handle much of this complexity for you under the hood. However, understanding the manual implementation is crucial for debugging and for scenarios where you need to integrate with a custom or legacy Identity Provider. The following example demonstrates how to construct the authorization URL with PKCE parameters.
Handling the Token Exchange
Once the user is redirected back to your application, the URL will contain an authorization code in the query parameters. Your application must extract this code and the state parameter from the URL. After validating that the state matches what you stored initially, you can proceed to request the access token from the backend or the Identity Provider directly.
The POST request to the token endpoint should be made using an application/x-www-form-urlencoded content type. It must include the grant_type set to authorization_code, the code itself, the original redirect_uri, the client_id, and most importantly, the unhashed code_verifier. If the Identity Provider validates everything successfully, it returns a JSON object containing the access_token, id_token, and potentially a refresh_token.
Security Best Practices and Strategic Trade-offs
While PKCE significantly improves security, it is not a silver bullet. Developers must still consider how they store the resulting access tokens in the browser. Storing tokens in LocalStorage makes them vulnerable to XSS, as any script running on your page can access that storage. A more secure approach is to use a Backend-for-Frontend (BFF) pattern where tokens are stored in secure, HttpOnly, SameSite cookies.
If you choose to handle tokens entirely on the client side, you must implement rigorous Content Security Policies to mitigate the risk of script injection. Additionally, you should use short-lived access tokens and implement refresh token rotation. Refresh token rotation ensures that every time a refresh token is used, it is invalidated and a new one is issued, which helps detect and prevent token theft.
Security is a layered discipline. Implementing PKCE solves the interception problem, but token storage and lifecycle management are equally critical to a robust defense strategy.
Another consideration is the use of the OpenID Connect (OIDC) layer on top of OAuth 2.0. While OAuth is concerned with authorization, OIDC provides identity information through the ID Token. Using PKCE with OIDC allows you to verify both who the user is and what they are allowed to do, all while maintaining the high security standards required for modern distributed systems.
Mitigating Common Pitfalls
One common mistake is using the plain code challenge method instead of S256. The plain method sends the verifier in the clear during the first step, which defeats the entire purpose of PKCE in most environments. Always default to S256 unless you are working with a legacy system that lacks SHA-256 support.
Another pitfall is failing to validate the state parameter. Without state validation, an application might process an authorization code that was injected into the browser session by an attacker. Always treat the state as a mandatory part of the handshake to ensure the integrity of the entire authentication lifecycle.
