Site, OAuth2 playground
How the playground exercises a full OAuth2 flow
The Site repo's /playground route is a working OAuth2 client. Operators can use it to:
- Verify a deployment's OAuth2 surface end-to-end.
- See decoded tokens.
- Demonstrate the flow without writing client code.
What it does
1. User clicks "Log in as customer (CIAM)" or "Log in as employee (IAM)"
2. Site generates PKCE pair + state
3. Stores verifier in HttpOnly cookie
4. Redirects to chosen Hydra (/oauth2/auth)
5. User authenticates via Hera
6. Returns to /callback with code + state
7. Site validates state, exchanges code for tokens
8. Renders the tokens, decoded ID token claims, scopesCode layout
site/src/app/playground/page.tsx:
export default function Playground() {
return (
<div>
<a href="/login/ciam">Log in as customer (CIAM)</a>
<a href="/login/iam">Log in as employee (IAM)</a>
<PlaygroundTokens /> {/* Reads server-set cookie, renders tokens */}
</div>
);
}site/src/app/login/ciam/route.ts:
export async function GET(request: Request) {
const codeVerifier = base64UrlEncode(crypto.randomBytes(32));
const codeChallenge = base64UrlEncode(
new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier)))
);
const state = base64UrlEncode(crypto.randomBytes(16));
cookies.set("playground_pkce", codeVerifier, { httpOnly: true, secure: true });
cookies.set("playground_state", state, { httpOnly: true, secure: true });
const url = new URL(`${CIAM_HYDRA}/oauth2/auth`);
url.search = new URLSearchParams({
client_id: PLAYGROUND_CIAM_CLIENT_ID,
response_type: "code",
scope: "openid profile email",
redirect_uri: `${SITE_BASE}/callback/ciam`,
code_challenge: codeChallenge,
code_challenge_method: "S256",
state,
}).toString();
return Response.redirect(url.toString());
}site/src/app/callback/ciam/route.ts:
export async function GET(request: Request) {
const url = new URL(request.url);
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const verifier = cookies.get("playground_pkce")?.value;
const expectedState = cookies.get("playground_state")?.value;
if (state !== expectedState) return Response.json({ error: "state mismatch" }, { status: 400 });
const response = await fetch(`${CIAM_HYDRA}/oauth2/token`, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: PLAYGROUND_CIAM_CLIENT_ID,
code: code!,
redirect_uri: `${SITE_BASE}/callback/ciam`,
code_verifier: verifier!,
}),
});
const tokens = await response.json();
cookies.set("playground_tokens", JSON.stringify(tokens), { httpOnly: true });
return Response.redirect(`${SITE_BASE}/playground`);
}PlaygroundTokens reads the cookie server-side and renders the decoded tokens.
OAuth2 client setup
The Site repo registers two OAuth2 clients during seeding:
playground-ciam, registered in CIAM Hydra.playground-iam, registered in IAM Hydra.
Both:
- Public clients (no secret).
- PKCE mandatory.
- Redirect URI:
https://<site-domain>/callback/<ciam|iam>.
For local dev: http://localhost:2000/callback/ciam etc.
The seeding happens in site/scripts/seed-playground-clients.ts, run as part of the site container's startup.
Token rendering
function PlaygroundTokens({ tokens }: { tokens: Tokens }) {
return (
<div>
<h3>Access Token (opaque)</h3>
<pre>{tokens.access_token}</pre>
<h3>ID Token (JWT, decoded)</h3>
<pre>{JSON.stringify(decodeJwt(tokens.id_token), null, 2)}</pre>
<h3>Refresh Token</h3>
<pre>{tokens.refresh_token ?? "—"}</pre>
<h3>Scopes</h3>
<pre>{tokens.scope}</pre>
</div>
);
}JWT decoding is purely client-side display logic, no signature verification (the issuer-validating part), since the goal is "show what was issued."
What it demonstrates
- The full Authorization Code + PKCE flow.
- The dual-domain split (CIAM and IAM are clearly separate playground actions).
- The shape of the issued tokens.
What it doesn't do
- Refresh token rotation (would require state, which is a different shape than "demo").
- Token introspection from your own backend (the operator can do this manually).
- Logout (a simple
Clear cookiesbutton works for the playground).
Removing the playground
For production deployments where the playground isn't useful, remove the routes from site/src/app/. The brochure and docs continue to work.
Related
- Integrate, OAuth2 PKCE
- Get Started, Your first OAuth2 client, alternative example.
- Repo map, Site