Authorization Code + PKCE Flow
The Authorization Code + PKCE flow is the recommended OAuth 2.0 pattern for all applications that authenticate real users. This page explains what PKCE is, why it was introduced, and how the complete flow works from start to finish — including the security properties at each step.
Why PKCE Exists
The original OAuth 2.0 Authorization Code flow (without PKCE) has a security vulnerability in public clients: code interception.
When Auris redirects back to your application with an authorization code (step 4 in the classic flow), that code travels through the browser’s URL bar. On mobile devices, a malicious application registered to handle the same custom URI scheme could intercept the redirect. Even on web applications, the code can appear in server logs, referrer headers, or browser history.
If an attacker intercepts the code, they can exchange it for tokens at the token endpoint. In the classic flow, there is nothing to stop them.
PKCE (Proof Key for Code Exchange, RFC 7636) fixes this by binding the authorization code to the specific client session that requested it. Only the client that originally generated the code verifier can exchange the code — even if an attacker has the code itself.
Auris enforces PKCE on all authorization code flows. The plain challenge method is rejected — only the S256 method (SHA-256) is accepted. There is no way to bypass PKCE in Auris.
The PKCE Mechanism
PKCE adds two values to the standard flow:
Code Verifier: A cryptographically random string, 43-128 characters long, using only unreserved URL characters (A-Z, a-z, 0-9, -, ., _, ~). This value is kept secret by the client.
Code Challenge: A transformed version of the code verifier, computed as:
code_challenge = BASE64URL(SHA256(ASCII(code_verifier)))The client sends the code challenge (the hash) to the authorization endpoint. When exchanging the code, the client sends the raw code verifier. Auris re-computes the challenge and confirms it matches. Since an attacker cannot reverse a SHA-256 hash, they cannot forge the verifier.
Step-by-Step Flow
Step 1: Generate Code Verifier
The client generates a cryptographically random code_verifier. This must be 43-128 characters from the alphabet [A-Za-z0-9-._~].
function generateCodeVerifier(): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'
const array = new Uint8Array(64)
crypto.getRandomValues(array)
return Array.from(array)
.map(b => chars[b % chars.length])
.join('')
}
const codeVerifier = generateCodeVerifier()
// Example: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"Store the code_verifier in sessionStorage (not localStorage) — it only needs to survive the redirect round-trip.
Step 2: Derive Code Challenge
Compute SHA-256(code_verifier) and Base64URL-encode the result:
async function generateCodeChallenge(verifier: string): Promise<string> {
const encoder = new TextEncoder()
const data = encoder.encode(verifier)
const digest = await crypto.subtle.digest('SHA-256', data)
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '')
}
const codeChallenge = await generateCodeChallenge(codeVerifier)
// Example: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"Step 3: Generate State
Generate a random state parameter for CSRF protection. This value is sent to the authorization server and must be verified when the code is returned.
function generateState(): string {
const array = new Uint8Array(16)
crypto.getRandomValues(array)
return btoa(String.fromCharCode(...array))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '')
}
const state = generateState()
sessionStorage.setItem('pkce_state', state)
sessionStorage.setItem('pkce_code_verifier', codeVerifier)Step 4: Redirect to Authorize Endpoint
Construct the authorization URL and redirect the browser:
const params = new URLSearchParams({
response_type: 'code',
client_id: 'your-client-id',
redirect_uri: 'https://app.yourdomain.com/callback',
state: state,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
scope: 'openid profile email',
})
window.location.href = `https://api.altovar.net/api/oauth/authorize?${params}`The browser navigates to the Auris hosted login page. From this point, the user interacts with Auris directly — your application is not involved.
Step 5: User Authenticates on Auris
On the Auris hosted login page, the user:
- Enters their email and password (or uses social login, magic link, etc.)
- Completes MFA if required by the tenant policy or adaptive risk scoring
- Sees a consent screen if the application is requesting sensitive scopes (first time only)
Your application code is not running during this step.
Step 6: Auris Redirects Back with Code
On successful authentication, Auris issues a short-lived authorization code (5 minutes) and redirects the browser back to your redirect_uri:
https://app.yourdomain.com/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz789abcThe authorization code is single-use. Any attempt to use it twice is rejected.
Step 7: Validate State (CSRF Protection)
Before doing anything with the code, compare the state returned in the query string with the one stored in sessionStorage:
const returnedState = new URLSearchParams(window.location.search).get('state')
const storedState = sessionStorage.getItem('pkce_state')
if (!returnedState || returnedState !== storedState) {
throw new Error('State mismatch — possible CSRF attack')
}This ensures the redirect is responding to your original request, not to a cross-site request injection.
Step 8: Exchange Code for Tokens
Retrieve the stored code_verifier and send it alongside the authorization code to the token endpoint:
const code = new URLSearchParams(window.location.search).get('code')
const codeVerifier = sessionStorage.getItem('pkce_code_verifier')
const response = await fetch('https://api.altovar.net/api/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code: code,
code_verifier: codeVerifier,
redirect_uri: 'https://app.yourdomain.com/callback',
client_id: 'your-client-id',
}),
})
const data = await response.json()Auris verifies: SHA256(code_verifier) === stored_code_challenge. If they match, tokens are issued.
Step 9: Receive Tokens
{
"ok": true,
"data": {
"accessToken": "eyJhbGciOiJSUzI1NiJ9...",
"refreshToken": "rt_...",
"idToken": "eyJhbGciOiJSUzI1NiJ9...",
"expiresIn": 900,
"tokenType": "Bearer"
}
}Clean up the PKCE values from sessionStorage:
sessionStorage.removeItem('pkce_state')
sessionStorage.removeItem('pkce_code_verifier')Store the tokens according to your security requirements (see Token Storage Best Practices).
Sequence Diagram
Security Properties
Property 1: Code Interception Resistance
If an attacker intercepts the authorization code in step 6 (via URL sniffing, malicious app, or log scraping), they still cannot exchange it for tokens. To exchange the code, they need the code_verifier. The code_verifier is never sent to Auris until step 8 — and at that point it goes directly over HTTPS to the token endpoint, not through the browser URL.
Property 2: CSRF Protection via State
The state parameter is a random value generated by your client and returned unchanged by Auris. If an attacker crafts a malicious redirect to your callback URL with a forged code, your application will detect that the state does not match the one it stored, and reject the flow.
Property 3: Single-Use Authorization Codes
Each authorization code can only be used once. If Auris detects a code replay attempt (the same code used twice), it rejects the second request and may invalidate any tokens already issued for that code.
Property 4: Redirect URI Binding
The redirect_uri used at exchange time must exactly match the one registered in the Auris Console for that application. An attacker cannot register their own redirect URI and have codes routed there.
Property 5: Short Code Lifetime
Authorization codes expire after 5 minutes. This limits the window in which an intercepted code could be misused.
Using the Auris SDK
If you are using an Auris SDK, all of the above is handled automatically. You do not need to manage PKCE parameters, storage, or the token exchange manually:
import { AurisClient } from '@auris/js'
const auris = new AurisClient({
domain: 'your-auris-domain.com',
clientId: 'your-client-id',
redirectUri: 'https://app.yourdomain.com/callback',
autoRefresh: true,
})
// On your login button click:
await auris.loginWithRedirect()
// On your callback page:
const result = await auris.handleRedirectCallback()
console.log(result.user) // The authenticated userThe SDK generates the code verifier and challenge, stores them in sessionStorage, validates the state on callback, exchanges the code, and stores the resulting tokens automatically.
The Auris SDK uses crypto.subtle.digest('SHA-256', ...) (Web Crypto API) for the S256 challenge computation in browsers, and crypto.createHash('sha256') in Node.js. Both are cryptographically secure native implementations — no third-party crypto dependency is needed.
Related Concepts
- OAuth 2.0 & OIDC — The underlying protocol standards
- Hosted Login (Universal Login) — How Auris uses PKCE in hosted login pages
- Tokens Explained — What tokens are issued after the PKCE exchange
- Hosted Login Guide — Integrate PKCE login in your application
- Authentication API — Token exchange and authorization endpoints
- JavaScript SDK —
loginWithRedirect()uses PKCE automatically - React SDK —
AurisProviderhandles the full PKCE flow