コンテンツにスキップ

Authorization Code Flow

このコンテンツはまだ日本語訳がありません。

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

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
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
maxAgenumberMaximum authentication age in seconds
acrValuesstringRequested authentication context class
resourcestring | string[]Resource indicator for the requested access token
audiencestringTarget audience for the requested access token
extraParamsRecord<string, string>Additional query parameters
exposeStatebooleanfalseReturn state and nonce in the result
useParbooleanfalseSatisfy the server’s PAR-required guard. Use client.par to create the request_uri
useJarbooleanfalseSatisfy the server’s JAR-required guard. Use JARBuilder to create request objects

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,
});

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

The returned TokenSet contains:

PropertyTypeDescription
accessTokenstringOAuth 2.0 access token
refreshTokenstring | undefinedRefresh token (if granted)
idTokenstring | undefinedOIDC ID token (if openid scope)
tokenType'Bearer' | 'DPoP'Token type
expiresAtnumberExpiration time (epoch seconds)
scopestring | undefinedGranted scopes
refreshTokenExpiresAtnumber | undefinedRefresh token expiration time when returned
refreshTokenExpiresInnumber | undefinedRefresh token lifetime in seconds when returned
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();
}

To send authorization parameters via the back-channel, use client.par explicitly:

const parResult = await client.par.push({
redirectUri: 'https://myapp.com/callback',
scope: 'openid profile email',
});
const url = await client.par.buildAuthorizationUrl(parResult.requestUri);

See Pushed Authorization Requests for details.

To send authorization parameters as a signed JWT, build the request object with JARBuilder and pass it according to your authorization server integration:

const requestObject = await jarBuilder.buildRequestObject({
clientId: 'my-app',
issuer: 'https://auth.example.com',
redirectUri: 'https://myapp.com/callback',
scope: 'openid profile',
state,
nonce,
codeChallenge,
codeChallengeMethod: 'S256',
});

See JAR & JARM for details.

Pass additional parameters to the authorization endpoint:

const { url } = await client.buildAuthorizationUrl({
redirectUri: 'https://myapp.com/callback',
extraParams: {
organization: 'org_123',
},
audience: 'https://api.example.com',
resource: 'https://api.example.com',
});

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.