SPA sign-in with PKCE
Authorization Code with PKCE (S256) and state, as a public client. You'll see the decoded ID token and what happened at each step.
Live demo, no account needed
Every case below signs in for real against HeyIdentity: you pick a test user instead of typing a password, and come back here with real, signed tokens. Plain browser JavaScript, no frameworks, no client secret.
Cases A, B, E and F share one sign-in against a Generic configuration, so once you're signed in you can try all of them. Case D signs in against a Microsoft-profile configuration through Entra-shaped URLs.
Authorization Code with PKCE (S256) and state, as a public client. You'll see the decoded ID token and what happened at each step.
Sign in once as Avery Admin and once as Riley User. The same app gets different roles, and the simulated admin area is allowed or denied.
The Microsoft profile serves Entra ID v2.0 URLs. Tokens carry tid, oid, ver and scp, so MSAL and Microsoft.Identity.Web work unchanged.
Asking for offline_access returns a refresh token. Redeem it for new tokens, and call the userinfo endpoint with the access token.
Sign out through the advertised end_session_endpoint with id_token_hint, post_logout_redirect_uri and state.
Signing in against , client heyidentity-demo.
Authorization Code with PKCE is the recommended flow for browser apps. There's no client secret, so there's nothing to leak.
The demo fetches the configuration's discovery document and takes every endpoint URL from it. No endpoint is hardcoded.
HeyIdentity shows the configuration's test users. Picking one is the whole "login". Users, roles and claims are configured per configuration.
Back here, the code and the original PKCE verifier are posted to the token endpoint. The tokens never leave this browser tab.
This is the actual flow the demo runs (js/demo.js). Point it at your own configuration's discovery URL after you sign up.
// Generic profile: /t/{tenantRouteId}/c/{configurationId}/.well-known/openid-configuration
// Microsoft profile: /{configurationId}/v2.0/.well-known/openid-configuration
const disco = await fetch(
'https://app.heyidentity.dev/t/0f88f277-dad3-4f86-a66f-f6fc918db2ac/c/6cdf13d4-1c4e-4cf4-a1c1-9e1fc6009026/.well-known/openid-configuration'
).then(r => r.json());
const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
const challenge = base64url(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier)));
const state = base64url(crypto.getRandomValues(new Uint8Array(16)));
sessionStorage.setItem('pkce', JSON.stringify({ verifier, state }));
location.assign(disco.authorization_endpoint + '?' + new URLSearchParams({
response_type: 'code',
client_id: 'heyidentity-demo',
redirect_uri: location.origin + '/callback.html',
scope: 'openid profile email offline_access',
state,
code_challenge: challenge,
code_challenge_method: 'S256',
}));
const query = new URLSearchParams(location.search);
const { verifier, state } = JSON.parse(sessionStorage.getItem('pkce'));
if (query.get('state') !== state) throw new Error('State mismatch');
const tokens = await fetch(disco.token_endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: 'heyidentity-demo',
code: query.get('code'),
redirect_uri: location.origin + '/callback.html',
code_verifier: verifier,
}),
}).then(r => r.json());
// tokens.id_token, tokens.access_token, tokens.refresh_token