Implement OAuth 2.0 Authorization Code Flow with PKCE in a Single‑Page App
Implement OAuth 2.0 Authorization Code Flow with PKCE in a single‑page app: register the client, generate code_challenge, handle redirects, exchange tokens, validate ID tokens, store securely, and rotate refresh tokens. Verify each step with network tools and CSRF tests.
14 Apr 2026, 05:17 UTC

Desired Outcome
The goal is to obtain a short‑lived access token and a rotating refresh token for a single‑page application (SPA) while keeping the user’s credentials out of the browser and protecting against CSRF, XSS, and code interception.
Prerequisites
- A registered OAuth 2.0 client with a
client_idand a pre‑registeredredirect_urithat useshttps://. - HTTPS enabled for all endpoints (auth, token, JWKS).
- Ability to generate cryptographically random strings (e.g.,
crypto.getRandomValues). - Access to the provider’s JWKS endpoint for ID token verification.
- Optional: a backend proxy to rotate refresh tokens if the provider does not support it.
Focused Procedure
-
Generate PKCE Parameters
In the SPA, create a
code_verifierand derive acode_challengeusing SHA‑256 and Base64URL encoding. Store thecode_verifierin a secure, in‑memory variable until the token request.// Generate a 128‑byte verifier const verifier = base64urlEncode(crypto.getRandomValues(new Uint8Array(128))); // Create the challenge const challenge = base64urlEncode(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))); -
Build the Authorization Request
Include
response_type=code,client_id,redirect_uri,scope(e.g.,openid profile email offline_access),state(cryptographically random, tied to the user session),nonce(for ID token validation),code_challenge, andcode_challenge_method=S256.GET https://auth.example.com/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=openid%20profile%20email%20offline_access&state=RANDOM_STATE&nonce=RANDOM_NONCE&code_challenge=CHALLENGE&code_challenge_method=S256 HTTP/1.1 Host: auth.example.com -
Handle the Redirect
When the provider redirects back to
redirect_uri, the URL will containcodeandstateas query parameters. Verify that the returnedstatematches the one stored in the session. If it does not, reject the response. -
Exchange Code for Tokens
Send a POST request to the token endpoint with
grant_type=authorization_code, thecode,redirect_uri,client_id, and the originalcode_verifier. Do not sendclient_secretfrom the SPA.POST https://auth.example.com/token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=AUTH_CODE&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&client_id=YOUR_CLIENT_ID&code_verifier=VERIFIERExpect a JSON response containing
access_token,refresh_token,id_token, andexpires_in. -
Validate the ID Token
Decode the JWT, fetch the provider’s JSON Web Key Set (JWKS) from
https://auth.example.com/.well‑known/jwks.json, and verify the signature. Then check the standard claims:issmatches the provider,audincludes yourclient_id,expis in the future, andnoncematches the one sent earlier.const {payload, header} = jwtDecode(idToken, {complete: true}); const jwk = await fetchJWKS(header.kid); const isValid = await verifySignature(idToken, jwk); -
Secure Token Storage
- Never write tokens to
localStorageorsessionStoragebecause they are accessible to any script on the page. - Use an in‑memory store (e.g., a JavaScript variable) for the access token during the session.
- Store the refresh token in an HttpOnly, Secure cookie with
SameSite=Laxif the SPA shares a domain with a backend that can rotate the token. - If no backend is available, keep the refresh token in memory and clear it on page unload; the user will have to re‑authenticate after a short period.
- Never write tokens to
-
Refresh Token Rotation
When the access token expires, call the token endpoint again with
grant_type=refresh_tokenand the currentrefresh_token. The provider should return a new refresh token. Replace the old one immediately in the secure cookie or memory. This reduces the risk of a stolen refresh token being used for a long time.POST https://auth.example.com/token Content-Type: application/x-www-form-urlencoded grant_type=refresh_token&refresh_token=CURRENT_REFRESH_TOKEN&client_id=YOUR_CLIENT_ID -
Error Handling
If the token endpoint returns an error (e.g.,
invalid_grant), clear all stored tokens, invalidate the session, and redirect the user to the login page. Log the error for audit purposes but avoid exposing sensitive details to the UI.
Expected Checks
- Network inspector: confirm that the authorization request contains
state,code_challenge, andcode_challenge_method=S256. - Token request: ensure the
code_verifiermatches the earlier challenge. - ID token: verify signature,
iss,aud,exp, andnonce. - Refresh token rotation: after each refresh, the new token should be different from the previous one.
- CSRF test: simulate a tampered
statevalue; the app should reject the response and not store any token.
Recovery Options
- If a token is missing or invalid, prompt the user to re‑authenticate.
- In case of a lost refresh token, the user must log in again; the SPA should clear any stale tokens and show a clear message.
- For XSS‑related token theft, ensure that the refresh token is never exposed to client‑side scripts. If a breach is detected, rotate the refresh token server‑side and force a re‑login.
Limitations & Practical Checklist
- This guide assumes the provider supports PKCE and refresh token rotation; check the provider’s documentation.
- If the SPA cannot set HttpOnly cookies (e.g., due to CORS restrictions), consider moving token handling to a backend proxy.
- Remember that SPA cookies are still vulnerable to CSRF unless
SameSiteis set; always pair with astateparameter. - Validate that the
redirect_uriexactly matches the registered one, including trailing slashes. - Use a tool like
curlor Postman to manually test the token exchange before integrating into the SPA.
Concrete Example – Configuring Auth0 for an SPA
Below is a minimal auth_config.json you might store on the client (never include secrets):
{
"domain": "dev-xyz.us.auth0.com",
"clientId": "abc123DEF456",
"redirectUri": "https://app.example.com/callback",
"audience": "https://api.example.com",
"scope": "openid profile email offline_access"
}
Auth0’s token endpoint automatically validates PKCE and returns a rotating refresh token when offline_access is requested.
Conclusion
By following this structured flow—PKCE, state, nonce, secure storage, and refresh token rotation—you protect your SPA from the most common OAuth 2.0 attack vectors while maintaining a smooth user experience.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.