I IntelliAuth Docs

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.

What "PKCE" actually buys you

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 flow, end to end

Sequence diagram: the browser clicks Sign in, the app redirects to /oauth2/authorize with the PKCE code_challenge, the user signs in at the IntelliAuth tenant, the tenant redirects the browser back to /callback with an auth code, the app exchanges the code at /oauth2/token with the code_verifier, and the tenant returns access + refresh + id tokens which the SDK stores.
The authorization-code-with-PKCE flow, end to end. The SDK does every step in this diagram for you.

What the SDK does for you

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

ts
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.

When you would touch the wire format

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

http
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:

http
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:

json
{
  "access_token": "...",
  "id_token": "...",
  "refresh_token": "...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid profile email"
}

Common pitfalls

  • 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 and machine-to-machine

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.

See also