Files
buzz-sheet/lib/auth/oidc-flow.test.ts
T
gunshiz 445865bf42
CI / Verify (push) Successful in 1m31s
CI / Build immutable images and deploy (push) Successful in 2m4s
fix(auth) : bind Sudloh sessions and stop OAuth redirect loops
2026-10-06 14:28:59 +07:00

78 lines
3.7 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { betterAuth } from "better-auth";
import { memoryAdapter } from "better-auth/adapters/memory";
import { genericOAuth } from "better-auth/plugins";
import { isSudlohCallback } from "./sudloh-callback";
const origin = "https://guide.test";
function createReplica(database: Record<string, Record<string, unknown>[]>, onSession: (sessionId: string) => void) {
return betterAuth({
baseURL: origin,
secret: "a-shared-test-secret-with-enough-entropy-123",
database: memoryAdapter(database),
rateLimit: { enabled: false },
databaseHooks: { session: { create: { after: async (
session: { id: string }, context: { path: string; params?: { id?: string } } | null,
) => {
if (isSudlohCallback(context)) onSession(session.id);
} } } },
plugins: [genericOAuth({ config: [{
providerId: "sudloh",
clientId: "test-client",
clientSecret: "test-secret",
authorizationUrl: "https://account.test/authorize",
tokenUrl: "https://account.test/token",
getToken: async () => ({ accessToken: "test-access-token", accessTokenExpiresAt: new Date(Date.now() + 3600_000) }),
getUserInfo: async () => ({ id: "test-subject", email: "[email protected]",
emailVerified: true, name: "Test User" }),
}] })],
});
}
describe("Sudloh OAuth callback", () => {
it("completes across replicas, binds the Guide session, and consumes state once", async () => {
const database = { user: [], session: [], account: [], verification: [] };
const bind = vi.fn();
const first = createReplica(database, bind);
const second = createReplica(database, bind);
const start = await first.handler(new Request(`${origin}/api/auth/sign-in/social`, {
method: "POST", headers: { Origin: origin, "Content-Type": "application/json" },
body: JSON.stringify({ provider: "sudloh", callbackURL: "/profile" }),
}));
expect(start.status).toBe(200);
const authorization = new URL((await start.json()).url);
expect(authorization.searchParams.get("code_challenge_method")).toBe("S256");
expect(authorization.searchParams.get("code_challenge")).toBeTruthy();
const stateCookie = start.headers.get("set-cookie")?.split(";", 1)[0];
expect(stateCookie).toBeTruthy();
const callbackURL = new URL(`${origin}/api/auth/callback/sudloh`);
callbackURL.searchParams.set("code", "test-code");
callbackURL.searchParams.set("state", authorization.searchParams.get("state")!);
const callback = await second.handler(new Request(callbackURL, {
headers: { Cookie: stateCookie! },
}));
expect(callback.status).toBe(302);
expect(callback.headers.get("location")).toBe("/profile");
expect(bind).toHaveBeenCalledOnce();
const sessionCookie = callback.headers.getSetCookie()
.find((cookie) => cookie.includes("session_token="))?.split(";", 1)[0];
const session = await second.api.getSession({ headers: new Headers({ Cookie: sessionCookie! }) });
expect(session?.user.email).toBe("[email protected]");
const replay = await first.handler(new Request(callbackURL, {
headers: { Cookie: stateCookie! },
}));
expect(replay.headers.get("location")).toContain("state_mismatch");
expect(bind).toHaveBeenCalledOnce();
});
it("rejects a callback without state before exchanging a code", async () => {
const bind = vi.fn();
const auth = createReplica({ user: [], session: [], account: [], verification: [] }, bind);
const response = await auth.handler(new Request(`${origin}/api/auth/callback/sudloh?code=test-code`));
expect(response.status).toBe(302);
expect(response.headers.get("location")).toContain("state_not_found");
expect(bind).not.toHaveBeenCalled();
});
});