#!/usr/bin/env bun /** * A stand-in for Supabase Auth: an RS256 issuer that publishes a JWKS at the * same path a Supabase project does, and mints access tokens on request. * * This exists so the *real* verification path is testable. The backend * verifies tokens with `createRemoteJWKSet` against * `$SUPABASE_URL/auth/v1/.well-known/jwks.json`, and the alternative to * standing that endpoint up locally is a test-only bypass in the backend - * an env var that makes it trust an unsigned token. That bypass would be a * permanent hole one misconfiguration away from production, and it would mean * the code paths under test are not the code paths that ship. * * Point the backend at it with `SUPABASE_URL=http://localhost:`. * * Usage as a server: * bun scripts/dev-auth.ts [--port 4545] * GET /auth/v1/.well-known/jwks.json the public keys * GET /token?sub=&email= a signed access token * * Usage from a test: * const issuer = await startDevAuthIssuer(); * const token = await issuer.mint({ sub: userId }); * issuer.stop(); */ import { SignJWT, exportJWK, generateKeyPair, calculateJwkThumbprint } from "jose"; export interface MintOptions { sub: string; email?: string; /** Seconds until expiry. Negative values mint an already-expired token. */ expiresIn?: number; /** Overrides the `aud` claim; the backend requires "authenticated". */ audience?: string; } export async function startDevAuthIssuer(port = 0) { const { publicKey, privateKey } = await generateKeyPair("RS256", { extractable: true }); const publicJwk = await exportJWK(publicKey); const kid = await calculateJwkThumbprint(publicJwk); const jwks = { keys: [{ ...publicJwk, kid, alg: "RS256", use: "sig" }] }; async function mint(options: MintOptions): Promise { const expiresIn = options.expiresIn ?? 3600; return new SignJWT({ sub: options.sub, email: options.email, role: "authenticated" }) .setProtectedHeader({ alg: "RS256", kid }) .setIssuedAt() .setAudience(options.audience ?? "authenticated") .setExpirationTime(Math.floor(Date.now() / 1000) + expiresIn) .sign(privateKey); } const server = Bun.serve({ port, async fetch(request) { const url = new URL(request.url); if (url.pathname === "/auth/v1/.well-known/jwks.json") { return Response.json(jwks); } if (url.pathname === "/token") { const sub = url.searchParams.get("sub"); if (!sub) return new Response("?sub= is required", { status: 400 }); const expiresIn = url.searchParams.get("expiresIn"); return Response.json({ access_token: await mint({ sub, email: url.searchParams.get("email") ?? undefined, expiresIn: expiresIn ? Number(expiresIn) : undefined, audience: url.searchParams.get("audience") ?? undefined, }), }); } return new Response("not found", { status: 404 }); }, }); return { url: `http://localhost:${server.port}`, port: server.port, kid, mint, /** A token signed by a *different* key, for testing rejection. */ async mintWithForeignKey(options: MintOptions): Promise { const other = await generateKeyPair("RS256", { extractable: true }); const otherJwk = await exportJWK(other.publicKey); return new SignJWT({ sub: options.sub, role: "authenticated" }) .setProtectedHeader({ alg: "RS256", kid: await calculateJwkThumbprint(otherJwk) }) .setIssuedAt() .setAudience(options.audience ?? "authenticated") .setExpirationTime(Math.floor(Date.now() / 1000) + (options.expiresIn ?? 3600)) .sign(other.privateKey); }, stop: () => server.stop(true), }; } if (import.meta.main) { const portFlag = process.argv.indexOf("--port"); const port = portFlag === -1 ? 4545 : Number(process.argv[portFlag + 1]); const issuer = await startDevAuthIssuer(port); console.log(`dev-auth issuer on ${issuer.url} (kid ${issuer.kid})`); console.log(` SUPABASE_URL=${issuer.url}`); }