Web app with Authorization Code and PKCE
This guide walks through signing a user into a web application from start to finish. The same flow applies to SPAs and mobile apps; only where you store tokens changes.
Building on Next.js? The Next.js SDK implements every step on this page for you.
1. Discover configuration
const issuer = 'https://account.example.com'
const configuration = await fetch(
`${issuer}/.well-known/openid-configuration`,
).then((response) => response.json())
Cache discovery metadata for a bounded period and refresh it normally. Do not hardcode signing keys.
2. Create state, nonce, and PKCE
Generate cryptographically random state, nonce, and code_verifier values. Store them in a secure, short-lived transaction bound to the browser session. Derive code_challenge as the base64url-encoded SHA-256 of the verifier.
const base64url = (bytes) =>
btoa(String.fromCharCode(...new Uint8Array(bytes)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '')
const random = () => base64url(crypto.getRandomValues(new Uint8Array(32)))
const state = random()
const nonce = random()
const codeVerifier = random()
const codeChallenge = base64url(
await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier)),
)
3. Redirect to authorization
const url = new URL(configuration.authorization_endpoint)
url.search = new URLSearchParams({
client_id: 'YOUR_CLIENT_ID',
redirect_uri: 'https://app.example.com/api/auth/callback',
response_type: 'code',
scope: 'openid profile email offline_access',
state,
nonce,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
ui_locales: 'fa en',
ui_theme: 'dark',
}).toString()
window.location.assign(url)
Raha Account's hosted pages handle login, signup, MFA, account selection, and consent. See sign-in and sessions for optional parameters such as prompt.
4. Handle the callback
Raha Account redirects back to your redirect_uri with code and state, or with error and error_description if the user cancelled or the request failed.
Reject the callback if state does not exactly match the stored transaction. Then exchange the code from your backend:
curl -X POST "$TOKEN_ENDPOINT" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'client_id=YOUR_CLIENT_ID' \
--data-urlencode 'code=RETURNED_CODE' \
--data-urlencode 'redirect_uri=https://app.example.com/api/auth/callback' \
--data-urlencode 'code_verifier=ORIGINAL_CODE_VERIFIER'
Confidential clients also authenticate with their registered method. See client authentication.
{
"access_token": "eyJ...",
"id_token": "eyJ...",
"refresh_token": "...",
"token_type": "Bearer",
"expires_in": 900,
"scope": "openid profile email offline_access"
}
5. Validate the ID token
Before trusting the ID token, verify:
- The signature, using a key from
jwks_urithat matches the token'skid. issequals the discovered issuer.audcontains yourclient_id.expis in the future.nonceequals the value you stored in step 2.
Use sub as the stable user identifier. Do not use email or phone numbers as keys; users can change them.
6. Call APIs and refresh tokens
Send the access token only to its intended audience:
curl "$USERINFO_ENDPOINT" -H "Authorization: Bearer $ACCESS_TOKEN"
When the access token expires, exchange the refresh token for a new one:
curl -X POST "$TOKEN_ENDPOINT" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'client_id=YOUR_CLIENT_ID' \
--data-urlencode 'refresh_token=REFRESH_TOKEN'
Keep refresh tokens and client secrets on the server. If refresh fails with invalid_grant, the session ended or the user revoked your application; sign the user in again.
7. Sign the user out
Redirect to the discovered end_session_endpoint with id_token_hint, a registered post_logout_redirect_uri, and optional state. See logout.