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
Section titled “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_verifierand acode_challenge = SHA256(code_verifier). - It sends the
code_challengewith the initial/oauth2/authorizerequest. - When it later exchanges the authorization code for tokens at
/oauth2/token, it includes the originalcode_verifier. The platform recomputes the SHA256, compares to the storedcode_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
Section titled “The flow, end to end”What the SDK does for you
Section titled “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:
const { loginWithRedirect } = useIntelliAuth()await loginWithRedirect()The SDK:
- Generates the
code_verifier+code_challenge. - Stores
code_verifierinsessionStorage(cleared on consume). - Generates a
stateparameter and stores it alongside. - Redirects the browser to
/oauth2/authorize. - Handles the
/callbackroute: verifiesstate, 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
Section titled “When you would touch the wire format”Three situations:
- 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.
- You are debugging a stuck redirect — the network tab shows you the
/oauth2/authorizeURL; understanding its parameters lets you spot mistakes. - 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=S256The 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/tokenContent-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"}Common pitfalls
Section titled “Common pitfalls”redirect_urimust match exactly. A trailing slash, a different port,httpvshttps— any difference is a refusal. Register every variant you need in the application's settings.stateis 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_verifiermust survive the redirect. Stash it insessionStorage(per-tab) rather thanlocalStorage(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/tokencall without a fresh/oauth2/authorize, the second exchange fails.
Native apps and machine-to-machine
Section titled “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.