TL;DR
This guide shows you how to securely authenticate Single Page Applications (SPAs) using the Authorization Code flow with Proof Key for Code Exchange (PKCE). This is the recommended method as it avoids storing credentials in the browser.
1. Understand the Flow
The Auth Code flow with PKCE involves these steps:
- Code Verifier Generation: The SPA generates a random string (the code verifier).
- Code Challenge Creation: The SPA creates a hash of the code verifier (the code challenge).
- Authorization Request: The SPA redirects the user to the authorization server with the code challenge.
- Authentication & Authorization: The user authenticates and authorizes the application on the authorization server.
- Redirection with Code: The authorization server redirects back to the SPA with an authorization code.
- Token Request: The SPA exchanges the authorization code for access tokens using the code verifier.
- Token Response: The authorization server validates the code verifier and returns access tokens (and optionally a refresh token).
2. Code Verifier & Challenge Generation
You’ll need a library to generate cryptographically secure random strings. Here’s an example using JavaScript:
function generateCodeVerifier(length = 128) {
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
function generateCodeChallenge(codeVerifier) {
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const hashBuffer = crypto.subtle.digest('SHA256', data);
return btoa(String.fromCharCode(...new Uint8Array(hashBuffer)));
}
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
Store the codeVerifier securely in your SPA’s session storage (not local storage!). The codeChallenge is sent to the authorization server.
3. Constructing the Authorization Request
Build the URL that redirects the user to the authorization server. Include these parameters:
response_type: Set tocodeclient_id: Your application’s client ID.redirect_uri: The URL the authorization server redirects back to after authentication.scope: The permissions your application requests (e.g.,openid profile email).code_challenge: The generated code challenge.code_challenge_method: Set toS256.
Example:
const authorizationEndpoint = 'https://your-auth-server/authorize';
const redirectUri = 'https://your-spa/callback';
const scope = 'openid profile email';
const authUrl = `${authorizationEndpoint}?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&code_challenge=${codeChallenge}&code_challenge_method=S256`;
window.location.href = authUrl;
4. Handling the Redirect (Callback)
After successful authentication, the authorization server redirects to your redirect_uri with an authorization code in the query parameters.
Extract the code from the URL.
5. Exchanging the Code for Tokens
Make a POST request to the token endpoint of your authorization server. Include these parameters:
grant_type: Set toauthorization_codecode: The authorization code received in the redirect.redirect_uri: Must match the one used in the authorization request.client_id: Your application’s client ID.code_verifier: The originalcodeVerifiergenerated in step 2.
Example (using fetch):
const tokenEndpoint = 'https://your-auth-server/token';
const requestBody = {
grant_type: 'authorization_code',
code: code,
redirect_uri: redirectUri,
client_id: clientId,
code_verifier: codeVerifier
};
fetch(tokenEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
})
.then(response => response.json())
.then(data => {
// Handle the access tokens (and refresh token if provided)
const accessToken = data.access_token;
// Store the access token securely
});
6. Security Considerations
- Never store credentials in local storage: Use session storage for the
codeVerifierand avoid storing sensitive information long-term. - HTTPS only: Ensure your SPA and authorization server communicate over HTTPS.
- Validate redirect URIs: Configure allowed redirect URIs on the authorization server to prevent open redirects.
- Protect against Cross-Site Request Forgery (CSRF): Implement CSRF protection mechanisms.