OAuth / OIDC (createOidcClient)
Flux OAuth de bout en bout contre un fournisseur fictif hébergé sur le même site, entièrement câblé avec @bext-stack/framework/oidc. PKCE, cookie de flux signé (un seul), échange de jeton et session HMAC signée — copiez la forme dans une vraie app et remplacez l'IdP par Google, GitHub ou le vôtre. Le CSRF est assuré par l'enveloppe de flux signée comparée au callback.
Astuce
Cette démo remplace ~250 lignes de crypto/cookies écrites à la main par un seul <code>createOidcClient</code>. La session est désormais <strong>signée par HMAC</strong> (l'ancienne démo posait un cookie JSON non signé) et l'échange de code utilise <strong>PKCE</strong>. Le facteur crypto est en JS pur, donc identique sur V8 et QuickJS.
Non connecté
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);