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.
Authorization Code + PKCE
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.
Authorization Code + PKCE is the recommended approach for browser-based apps. No client secret needed.
Your app creates a random code_verifier and hashes it (SHA-256) into a code_challenge. Only the challenge is sent over the wire.
The browser redirects to /authorize with response_type=code, your client_id, redirect_uri, and the PKCE challenge.
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.
Point your frontend at app.heyidentity.dev and register a client. The discovery document gives you all endpoint URLs automatically.
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" ]
}
]
}
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
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}`;
}
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
Fill in your FakeIdp details and click Sign in. You will be redirected to the identity provider and returned here with decoded claims.