Skip to content

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 user
window.location.href = url;

The SDK automatically:

  • Generates a cryptographically secure state parameter (CSRF protection)
  • Generates a nonce (replay attack prevention)
  • Creates a PKCE code_verifier and code_challenge (S256 method)
  • Stores state, nonce, and code verifier in storage for later validation

Options

ParameterTypeDefaultDescription
redirectUristringRequiredURL to redirect after authentication
scopestring'openid profile'Space-separated scopes
responseType'code' | 'none''code'OAuth 2.0 response type
promptstring'none', 'login', 'consent', or 'select_account'
loginHintstringPre-fill the login identifier
acrValuesstringRequested authentication context class
extraParamsRecord<string, string>Additional query parameters
exposeStatebooleanfalseReturn state and nonce in the result
useParbooleanfalseUse Pushed Authorization Requests
useJarbooleanfalseUse 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 page
const tokens = await client.handleCallback(window.location.href);
console.log(tokens.accessToken); // Access token
console.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:

PropertyTypeDescription
accessTokenstringOAuth 2.0 access token
refreshTokenstring | undefinedRefresh token (if granted)
idTokenstring | undefinedOIDC ID token (if openid scope)
tokenType'Bearer'Token type
expiresAtnumberExpiration time (epoch seconds)
scopestring | undefinedGranted scopes

Complete Flow Example

import { createAuthrimClient } from '@authrim/core';
// Initialize client
const 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 CodeDescriptionRecovery
invalid_stateState mismatch (possible CSRF attack)Restart login
expired_stateState expired (user took too long)Restart login
missing_codeNo authorization code in callbackCheck redirect URL
invalid_grantCode exchange failedRestart login
access_deniedUser denied consentInform 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