OAuth / OIDC (createOidcClient)

End-to-end OAuth flow against a mock provider on the same site, wired entirely with @bext-stack/framework/oidc. PKCE, a single signed flow cookie, token exchange and an HMAC-signed session — copy the shape into a real app and swap the IdP for Google, GitHub, or your own. CSRF is enforced by the signed flow envelope compared at callback.

Tip
This demo replaces ~250 lines of hand-rolled crypto/cookies with a single <code>createOidcClient</code>. The session is now <strong>HMAC-signed</strong> (the old demo shipped an unsigned JSON cookie) and the code exchange uses <strong>PKCE</strong>. The crypto is pure JS, so it runs identically on V8 and QuickJS.

Not signed in

src/app/examples/oauth-mock/page.tsxTSX
import { createOidcClient } from "@bext-stack/framework/oidc";

// One configured client replaces ~250 lines of hand-rolled SHA-256/HMAC,
// cookie parsing, PKCE, flow-state and session code. Byte-compatible with
// the copies it replaces, so live sessions keep validating.
const client = createOidcClient({
  issuer: "https://auth.example.com",
  clientId: "my-app",
  redirectUri: "https://my-app.com/auth/callback",
  sessionSecret: () => process.env.SESSION_SECRET,  // lazy resolver
  sessionCookieName: "my_session",
  flowCookieName: "my_flow",
  flowCarrier: "ret",   // 303 back to the saved path (vs "popup")
});

// Login: PKCE + state + nonce in one signed flow cookie, then 303 to the IdP.
export async function action({ request }) {
  const verifier = client.generateCodeVerifier();
  const challenge = await client.generateCodeChallenge(verifier);
  const state = client.generateState(), nonce = client.generateNonce();
  const flow = client.buildFlowState({ pkce: verifier, state, nonce, ret: "/" });
  return new Response(null, { status: 303, headers: {
    Location: client.buildAuthorizeUrl({ codeChallenge: challenge, state, nonce }),
    "set-cookie": client.flowEnvelopeCookie(flow),
  }});
}

// Callback: verify state, exchange the code (PKCE), sign the session.
export async function loader({ request }) {
  const flow = client.readFlowEnvelope(request);
  const code = new URL(request.url).searchParams.get("code");
  if (!flow || flow.state !== url.searchParams.get("state")) throw fail("state");
  const tok = await client.exchangeCode({ code, codeVerifier: flow.pkce });
  const claims = client.decodeIdToken(tok.id_token);
  const session = client.packSession({ sub: claims.sub, name: claims.name,
    email: claims.email, iat: now, exp: now + 600 });
  const headers = new Headers({ Location: flow.ret });
  headers.append("Set-Cookie", client.sessionCookie(session));
  headers.append("Set-Cookie", client.clearFlowEnvelope());
  throw new Response(null, { status: 303, headers });
}

// Any route: read + verify the signed session (tampered/expired → null).
const session = client.readSession(request);