Skip to content

Authorization code with PKCE — for web apps

If you are building a single-page app or a server-rendered web app, this is the flow you want. The SDKs default to it. You almost never need to think about the wire-level details — but the details matter when something goes wrong, so they live here.

PKCE (Proof Key for Code Exchange, RFC 7636) is a small protocol addition that makes the authorization-code flow safe to use without a client secret. The browser cannot keep a secret; PKCE replaces the secret with a one-time code_verifier that the SDK generates per sign-in.

The trade is:

  • The SDK generates a random code_verifier and a code_challenge = SHA256(code_verifier).
  • It sends the code_challenge with the initial /oauth2/authorize request.
  • When it later exchanges the authorization code for tokens at /oauth2/token, it includes the original code_verifier. The platform recomputes the SHA256, compares to the stored code_challenge, and refuses the exchange on mismatch.

An attacker who intercepts the authorization code (say, via a malicious browser extension) cannot exchange it for tokens — they do not have the code_verifier. PKCE makes the code single-use to the original session.

The authorization-code-with-PKCE flow, end to end. The SDK does every step in this diagram for you.

If you are using @intelliauth/react-sdk or a similar SDK, you get the entire flow above by calling one method:

const { loginWithRedirect } = useIntelliAuth()
await loginWithRedirect()

The SDK:

  • Generates the code_verifier + code_challenge.
  • Stores code_verifier in sessionStorage (cleared on consume).
  • Generates a state parameter and stores it alongside.
  • Redirects the browser to /oauth2/authorize.
  • Handles the /callback route: verifies state, exchanges the code, stores tokens.
  • Sets up silent refresh.

You write the "Sign in" button. The SDK writes the rest.

Three situations:

  1. You are not using an IntelliAuth SDK — for example, you are building a Rust backend and want to do the OAuth dance manually. The wire format below is the contract.
  2. You are debugging a stuck redirect — the network tab shows you the /oauth2/authorize URL; understanding its parameters lets you spot mistakes.
  3. You are integrating with a third-party tool that drives the flow (a migration shim from a previous identity provider, certain testing harnesses).

Wire-level: send the user's browser to

GET https://<your-tenant-url>/oauth2/authorize
?response_type=code
&client_id=<your-client-id>
&redirect_uri=<your-callback-url>
&scope=openid+profile+email
&state=<random-anti-csrf>
&code_challenge=<base64url-sha256-of-verifier>
&code_challenge_method=S256

The user signs in and consents. On success the platform redirects to <your-callback-url>?code=<auth-code>&state=<the-state-you-sent>.

Then exchange the code from your backend or browser:

POST https://<your-tenant-url>/oauth2/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&client_id=<your-client-id>
&code=<auth-code>
&redirect_uri=<your-callback-url>
&code_verifier=<the-original-verifier>

Response:

{
"access_token": "...",
"id_token": "...",
"refresh_token": "...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile email"
}
  • redirect_uri must match exactly. A trailing slash, a different port, http vs https — any difference is a refusal. Register every variant you need in the application's settings.
  • state is not optional. It is your CSRF defence. The SDK generates and verifies it for you; if you implement the flow by hand, do not skip it.
  • code_verifier must survive the redirect. Stash it in sessionStorage (per-tab) rather than localStorage (shared across tabs) so two concurrent sign-ins on the same browser don't trample each other.
  • One code, one exchange. Authorization codes are single-use. If your code retries the /oauth2/token call without a fresh /oauth2/authorize, the second exchange fails.

Native apps (mobile, desktop) use the same flow with platform-specific browser handoff — see the native-app companion topic. Machine-to-machine integrations (no human present) use client credentials instead, which is a different topic.