JAVASCRIPT
Implementing OAuth 2.0 PKCE Flow for SPAs
Securely integrate OAuth 2.0 with PKCE (Proof Key for Code Exchange) in your Single-Page Application to authorize users without exposing client secrets.
// Helper to generate a random string for code_verifier
function generateRandomString(length) {
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let randomString = '';
for (let i = 0; i < length; i++) {
randomString += possible.charAt(Math.floor(Math.random() * possible.length));
}
return randomString;
}
// Helper to base64url encode an array buffer
function base64urlencode(buffer) {
const charCodes = new Uint8Array(buffer);
const binary = String.fromCharCode(...charCodes);
return btoa(binary)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
// Helper to generate a code_challenge from a code_verifier
async function generateCodeChallenge(codeVerifier) {
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await window.crypto.subtle.digest('SHA-256', data);
return base64urlencode(digest);
}
// --- Main PKCE Flow ---
async function initiatePKCEAuth(clientId, redirectUri, authEndpoint, tokenEndpoint, scope) {
const codeVerifier = generateRandomString(128);
const codeChallenge = await generateCodeChallenge(codeVerifier);
// Store codeVerifier for later use (e.g., in localStorage)
localStorage.setItem('pkce_code_verifier', codeVerifier);
const params = new URLSearchParams({
response_type: 'code',
client_id: clientId,
redirect_uri: redirectUri,
scope: scope,
code_challenge_method: 'S256',
code_challenge: codeChallenge
});
window.location.href = `${authEndpoint}?${params.toString()}`;
}
async function exchangePKCECodeForTokens(clientId, redirectUri, tokenEndpoint, code) {
const codeVerifier = localStorage.getItem('pkce_code_verifier');
if (!codeVerifier) {
throw new Error('Code Verifier not found. Authentication flow interrupted.');
}
localStorage.removeItem('pkce_code_verifier'); // Clear after use
const params = new URLSearchParams({
grant_type: 'authorization_code',
client_id: clientId,
redirect_uri: redirectUri,
code: code,
code_verifier: codeVerifier
});
const response = await fetch(tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params.toString()
});
if (!response.ok) {
throw new Error(`Token exchange failed: ${response.statusText}`);
}
const tokenData = await response.json();
// Store and use tokenData (e.g., access_token, id_token)
console.log('Successfully obtained tokens:', tokenData);
return tokenData;
}
// Example Usage (assuming you are on the redirect_uri after authorization):
// const CLIENT_ID = 'your-client-id';
// const REDIRECT_URI = window.location.origin + '/callback';
// const AUTH_ENDPOINT = 'https://accounts.example.com/oauth/authorize';
// const TOKEN_ENDPOINT = 'https://accounts.example.com/oauth/token';
// const SCOPE = 'openid profile email';
// // On initial button click to log in:
// // initiatePKCEAuth(CLIENT_ID, REDIRECT_URI, AUTH_ENDPOINT, TOKEN_ENDPOINT, SCOPE);
// // On redirect_uri page load:
// // const urlParams = new URLSearchParams(window.location.search);
// // const authCode = urlParams.get('code');
// // if (authCode) {
// // exchangePKCECodeForTokens(CLIENT_ID, REDIRECT_URI, TOKEN_ENDPOINT, authCode)
// // .catch(err => console.error('PKCE Token Exchange Error:', err));
// // }
How it works: This JavaScript snippet demonstrates the client-side implementation of the OAuth 2.0 PKCE (Proof Key for Code Exchange) flow. It involves generating a `code_verifier` and its `code_challenge`, redirecting the user to the authorization server, and then exchanging the authorization code for tokens using the stored `code_verifier`. PKCE is crucial for Single-Page Applications (SPAs) as it enhances security by mitigating authorization code interception attacks without requiring a client secret, making it suitable for public clients.