Olympus Docs
InternalsSite

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, scopes

Code 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 cookies button 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.

On this page