Skip to content

Authorization Code Flow + PKCE

The Authorization Code flow is the recommended OAuth2 grant type for web applications, SPAs, and native apps. Autentico supports PKCE (RFC 7636) for public clients that cannot securely store a client secret.

Callback query parameters already present in the registered redirect_uri are preserved. The response URL encodes code and state as query values, so special characters in state round-trip unchanged, including when login requires MFA. Clients should decode query parameters with their URL library and compare the returned state with the original value.

Request objects passed through request or request_uri are unsupported. Nonempty values return request_not_supported or request_uri_not_supported, respectively, before login or SSO. See discovery metadata.

Authorization results use query parameters. A nonempty unsupported response_mode, including fragment or form_post, returns invalid_request before login or SSO. Discovery advertises only ["query"].

sequenceDiagram
    actor User
    participant App as Relying Party
    participant Browser
    participant IdP as Autentico

    App->>Browser: Redirect to /oauth2/authorize with PKCE params
    Browser->>IdP: GET /oauth2/authorize
    IdP->>Browser: Render login page
    User->>Browser: Enter credentials
    Browser->>IdP: POST /oauth2/login
    Note over IdP: MFA challenge if enabled
    IdP->>Browser: 302 → redirect_uri?code=AUTH_CODE&state=...
    Browser->>App: Deliver AUTH_CODE
    App->>IdP: POST /oauth2/token (code + code_verifier)
    IdP->>App: { access_token, id_token, refresh_token }

Omitting code_challenge_method means plain (RFC 7636 §4.3). With the default S256 enforcement, that request returns invalid_request at /authorize. When enforcement is disabled for a legacy integration, an omitted method uses the verifier itself as the challenge, and the token exchange compares them directly.

PKCE is required for public clients and recommended for all clients. Unsupported code_challenge_method values, or a method without a challenge, are rejected at /authorize with invalid_request before login or SSO code issuance. Use S256; plain is accepted only when S256 enforcement is disabled.

function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return btoa(String.fromCharCode(...array))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
async function generateCodeChallenge(verifier) {
const data = new TextEncoder().encode(verifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
const codeVerifier = generateCodeVerifier();
// Store codeVerifier for the token exchange step
sessionStorage.setItem('code_verifier', codeVerifier);
const codeChallenge = await generateCodeChallenge(codeVerifier);

Redirect the user’s browser to the authorization endpoint:

GET /oauth2/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/callback
&scope=openid profile email
&state=RANDOM_STATE_VALUE
&nonce=RANDOM_NONCE_VALUE
&code_challenge=PKCE_CHALLENGE
&code_challenge_method=S256
Parameter Required Description
response_type Yes Must be code; other values return unsupported_response_type, including on SSO
client_id Yes Your registered client ID
response_mode Optional Only query is supported; omission or an empty value uses query encoding
redirect_uri Yes Must exactly match a registered redirect URI
scope Yes Space-separated scopes. Must include openid for OIDC.
state Recommended Opaque value for CSRF protection. Returned unchanged.
nonce Recommended Opaque value embedded in the ID token to prevent replay attacks.
code_challenge Required for public clients For S256, the base64url-encoded SHA-256 hash of code_verifier; for plain, the verifier itself
code_challenge_method Optional S256 (recommended) or plain; omission means plain and is rejected when S256 enforcement is enabled

Autentico renders the login page. After successful authentication (including MFA if enabled), the user is redirected to your redirect_uri:

https://yourapp.com/callback
?code=AUTHORIZATION_CODE
&state=RANDOM_STATE_VALUE

Verify that state matches what you sent. The authorization code is short-lived (default 10 minutes) and single-use. Concurrent exchanges have one winner: marking the code used and saving its token/session pair happen in one transaction. A storage failure rolls back the exchange so it can be retried. Replaying a used code with valid client, redirect and PKCE proof rejects the request and revokes tokens linked to that browser session for the client, including refreshed tokens. Legacy codes without browser-session linkage use a broader user/client fallback for code and refresh grants.

Exchange the authorization code for tokens at the token endpoint.

Terminal window
curl -X POST https://auth.example.com/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "client_id=YOUR_CLIENT_ID" \
-d "code=AUTHORIZATION_CODE" \
-d "redirect_uri=https://yourapp.com/callback" \
-d "code_verifier=YOUR_CODE_VERIFIER"

Response:

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}

The id_token is a JWT containing identity claims about the authenticated user. See Token Structure & Claims for details.

Autentico also registers the Keycloak-compatible token endpoint:

POST /oauth2/protocol/openid-connect/token

This allows existing Keycloak client integrations to work by changing only the base URL.

The token endpoint rejects code_verifier when the authorization request had no code_challenge, including confidential clients, as required by RFC 9700 §4.8.2. Rejection returns invalid_grant without consuming the code. Confidential clients that omit PKCE continue exchanging codes without a verifier.