Authorization Code + PKCE

Frontend authentication with a fake identity provider.

A working demo built with vanilla JavaScript — no frameworks, no build tools. Configure your client ID and sign in to see the full OIDC flow in your browser.

How it works

Authorization Code + PKCE is the recommended approach for browser-based apps. No client secret needed.

1

Generate a PKCE challenge

Your app creates a random code_verifier and hashes it (SHA-256) into a code_challenge. Only the challenge is sent over the wire.

2

Redirect to authorization

The browser redirects to /authorize with response_type=code, your client_id, redirect_uri, and the PKCE challenge.

3

Exchange the code

After sign-in the IdP redirects back with an authorization code. Your app posts it along with the original code_verifier to /token to receive the ID token.

Configure your app

Point your frontend at app.heyidentity.dev and register a client. The discovery document gives you all endpoint URLs automatically.

1. Register your client

Add your client to the FakeIdp configuration. The Id becomes your client_id.

{
  "Clients": [
    {
      "Id": "my-app",
      "RedirectUris": [ "http://localhost:5173/callback" ],
      "AllowedScopes": [ "openid", "profile" ],
      "ExposedScopes":  [ "openid", "profile" ]
    }
  ]
}

2. Discover endpoints

Fetch the discovery document once at startup — no hardcoded endpoint URLs.

const IDP_URL = 'https://app.heyidentity.dev';

const disco = await fetch(
  `${IDP_URL}/.well-known/openid-configuration`
).then(r => r.json());

// disco.authorization_endpoint  →  redirect the user here
// disco.token_endpoint           →  exchange the code here

3. Start the PKCE flow

Use the Web Crypto API — available in all modern browsers, no dependencies required.

function base64url(buf) {
  return btoa(String.fromCharCode(...new Uint8Array(buf)))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

function generateVerifier() {
  return base64url(crypto.getRandomValues(new Uint8Array(32)).buffer);
}

async function generateChallenge(verifier) {
  const digest = await crypto.subtle.digest(
    'SHA-256', new TextEncoder().encode(verifier)
  );
  return base64url(digest);
}

async function startAuth(clientId, redirectUri) {
  const verifier  = generateVerifier();
  const challenge = await generateChallenge(verifier);
  const state     = base64url(crypto.getRandomValues(new Uint8Array(16)).buffer);

  sessionStorage.setItem('pkce_verifier', verifier);
  sessionStorage.setItem('pkce_state',    state);

  const params = new URLSearchParams({
    response_type:         'code',
    client_id:             clientId,
    redirect_uri:          redirectUri,
    scope:                 'openid profile',
    state,
    code_challenge:        challenge,
    code_challenge_method: 'S256',
  });

  window.location.href = `${disco.authorization_endpoint}?${params}`;
}

4. Exchange the code on the callback page

Read code and state from the URL, verify state, then POST to the token endpoint.

const params   = new URLSearchParams(location.search);
const code     = params.get('code');
const state    = params.get('state');

if (state !== sessionStorage.getItem('pkce_state')) {
  throw new Error('State mismatch — possible CSRF');
}

const tokens = await fetch(disco.token_endpoint, {
  method:  'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type:    'authorization_code',
    code,
    redirect_uri:  redirectUri,
    client_id:     clientId,
    code_verifier: sessionStorage.getItem('pkce_verifier'),
  }),
}).then(r => r.json());

// tokens.id_token     →  JWT with user claims
// tokens.access_token
CORS note: the token endpoint must allow cross-origin requests from your app's origin. In heyidentity.dev this is enabled for registered clients.

Live demo

Fill in your FakeIdp details and click Sign in. You will be redirected to the identity provider and returned here with decoded claims.