Authorization Code Flow
Overview
The Authorization Code Flow with PKCE is the recommended authentication method for public clients (SPAs, native apps). @authrim/core implements the complete flow, including state management, PKCE generation, and token exchange.
sequenceDiagram
participant U as User
participant C as Client
participant AS as Auth Server
U->>C: 1. Login
C->>AS: 2. Authorization URL + PKCE
AS->>U: 3. Login page
U->>AS: 4. Authenticate
AS->>C: 5. Redirect with auth code
C->>AS: 6. Exchange code + verifier
AS->>C: 7. Tokens
C->>U: 8. Logged in
Building the Authorization URL
Use buildAuthorizationUrl() to generate the authorization URL with PKCE and state parameters:
const { url } = await client.buildAuthorizationUrl({ redirectUri: 'https://myapp.com/callback', scope: 'openid profile email',});
// Redirect the userwindow.location.href = url;The SDK automatically:
- Generates a cryptographically secure
stateparameter (CSRF protection) - Generates a
nonce(replay attack prevention) - Creates a PKCE
code_verifierandcode_challenge(S256 method) - Stores state, nonce, and code verifier in storage for later validation
Options
| Parameter | Type | Default | Description |
|---|---|---|---|
redirectUri | string | Required | URL to redirect after authentication |
scope | string | 'openid profile' | Space-separated scopes |
responseType | 'code' | 'none' | 'code' | OAuth 2.0 response type |
prompt | string | — | 'none', 'login', 'consent', or 'select_account' |
loginHint | string | — | Pre-fill the login identifier |
acrValues | string | — | Requested authentication context class |
extraParams | Record<string, string> | — | Additional query parameters |
exposeState | boolean | false | Return state and nonce in the result |
usePar | boolean | false | Use Pushed Authorization Requests |
useJar | boolean | false | Use JWT Secured Authorization Request |
Accessing State and Nonce
If you need the generated state and nonce values (e.g., for server-side validation), set exposeState: true:
const { url, state, nonce } = await client.buildAuthorizationUrl({ redirectUri: 'https://myapp.com/callback', exposeState: true,});Handling the Callback
After the user authenticates, the authorization server redirects back to your redirectUri with an authorization code. Use handleCallback() to exchange it for tokens:
// On your callback pageconst tokens = await client.handleCallback(window.location.href);
console.log(tokens.accessToken); // Access tokenconsole.log(tokens.idToken); // ID token (if openid scope)console.log(tokens.refreshToken); // Refresh token (if granted)console.log(tokens.expiresAt); // Expiration timestamp (epoch seconds)The SDK automatically:
- Parses the authorization code and state from the callback URL
- Validates the state against the stored value (CSRF protection)
- Exchanges the code using the stored PKCE code verifier
- Saves the received tokens to storage
TokenSet
The returned TokenSet contains:
| Property | Type | Description |
|---|---|---|
accessToken | string | OAuth 2.0 access token |
refreshToken | string | undefined | Refresh token (if granted) |
idToken | string | undefined | OIDC ID token (if openid scope) |
tokenType | 'Bearer' | Token type |
expiresAt | number | Expiration time (epoch seconds) |
scope | string | undefined | Granted scopes |
Complete Flow Example
import { createAuthrimClient } from '@authrim/core';
// Initialize clientconst client = await createAuthrimClient({ issuer: 'https://auth.example.com', clientId: 'my-app', crypto: myCryptoProvider, storage: myStorageProvider, http: myHttpClient,});
// === Step 1: Start login ===async function login() { const { url } = await client.buildAuthorizationUrl({ redirectUri: 'https://myapp.com/callback', scope: 'openid profile email', }); window.location.href = url;}
// === Step 2: Handle callback ===async function handleCallback() { try { const tokens = await client.handleCallback(window.location.href); console.log('Login successful');
// Get user info const user = await client.getUser(); console.log('User:', user.name, user.email); } catch (error) { console.error('Login failed:', error); }}
// === Step 3: Use access token ===async function callApi() { const accessToken = await client.token.getAccessToken();
const response = await fetch('https://api.example.com/data', { headers: { Authorization: `Bearer ${accessToken}` }, }); return response.json();}Advanced Options
Using with PAR
To send authorization parameters via the back-channel (Pushed Authorization Requests):
const { url } = await client.buildAuthorizationUrl({ redirectUri: 'https://myapp.com/callback', usePar: true,});See Pushed Authorization Requests for details.
Using with JAR
To send authorization parameters as a signed JWT:
const { url } = await client.buildAuthorizationUrl({ redirectUri: 'https://myapp.com/callback', useJar: true,});See JAR & JARM for details.
Custom Parameters
Pass additional parameters to the authorization endpoint:
const { url } = await client.buildAuthorizationUrl({ redirectUri: 'https://myapp.com/callback', extraParams: { audience: 'https://api.example.com', organization: 'org_123', },});Error Handling
Common errors during the authorization code flow:
| Error Code | Description | Recovery |
|---|---|---|
invalid_state | State mismatch (possible CSRF attack) | Restart login |
expired_state | State expired (user took too long) | Restart login |
missing_code | No authorization code in callback | Check redirect URL |
invalid_grant | Code exchange failed | Restart login |
access_denied | User denied consent | Inform user |
import { AuthrimError } from '@authrim/core';
try { const tokens = await client.handleCallback(callbackUrl);} catch (error) { if (error instanceof AuthrimError) { switch (error.code) { case 'invalid_state': case 'expired_state': // Restart login await login(); break; case 'access_denied': // User denied showMessage('Access was denied'); break; default: console.error('Auth error:', error.code, error.message); } }}See Error Handling for comprehensive error management.
Next Steps
- Silent Authentication — Restore sessions without user interaction
- Token Management — Manage access tokens and refresh
- DPoP — Bind tokens to cryptographic keys