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.

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_uri that matches the token's kid.
  • iss equals the discovered issuer.
  • aud contains your client_id.
  • exp is in the future.
  • nonce equals 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.

Was this page helpful?