OAuth (mock provider)
End-to-end OAuth flow against a mock provider that lives on the same site. App ↔ IdP ↔ callback all wired through the same bext routes — copy the shape into a real app and swap the IdP URL for Google, GitHub, or your own. CSRF is enforced via a one-shot oauth_state cookie compared at callback.
Tip
The <code>oauth_state</code> cookie must be <code>HttpOnly</code> and <code>SameSite=Lax</code>: HttpOnly prevents JS from reading it (resists XSS), Lax prevents it from being sent on cross-site sub-resource requests. Without both, the CSRF check is ineffective.
Not signed in
// Three pieces:
//
// 1. /examples/oauth-mock — the app. Login button mints a
// CSRF state cookie + 303s to the
// IdP with redirect_uri + state.
//
// 2. /examples/mock-idp-login — the IdP's "consent" UI. User
// picks an identity; IdP 303s
// back to redirect_uri with
// ?code=...&state=...
//
// 3. /examples/oauth-mock/callback — exchanges the code at /api/mock-idp
// (real apps POST server→server),
// verifies state matches the
// cookie (CSRF), sets a session
// cookie, redirects to the app.
// Login action mints state, sets cookie, redirects to IdP.
export async function action({ request }) {
const state = randomState();
return new Response(null, { status: 303, headers: {
Location: `/examples/mock-idp-login?redirect_uri=${cb}&state=${state}`,
"set-cookie": "oauth_state=" + state + "; HttpOnly; SameSite=Lax",
}});
}
// Callback exchanges code, verifies state, sets session.
export async function loader({ request }) {
const url = new URL(request.url);
const code = url.searchParams.get("code");
if (cookies.oauth_state !== url.searchParams.get("state")) throw fail("state_mismatch");
const id = await (await fetch("/api/mock-idp?code=" + code)).json();
const headers = new Headers({ Location: "/examples/oauth-mock" });
headers.append("Set-Cookie", "oauth_session=" + JSON.stringify(id) + "; HttpOnly");
headers.append("Set-Cookie", "oauth_state=; Max-Age=0");
throw new Response(null, { status: 303, headers });
}