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
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);