import { createSign, generateKeyPairSync, type KeyObject } from "node:crypto"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createSessionCookieValue, parseSessionCookieValue, type AuthSession } from "@/lib/auth/session"; import { type AuthRuntimeConfig } from "@/lib/auth/config"; import { clearJwksCacheForTests, userFromClaims, verifyAuthJwt } from "@/lib/server/auth/jwt"; type TestJwk = JsonWebKey & { kid?: string; alg?: string; use?: string; }; const authConfig: AuthRuntimeConfig = { required: true, configured: true, missing: [], authBaseUrl: "https://gateway.example.com/auth", authorizeUrl: "https://gateway.example.com/auth/oauth2/authorize", tokenUrl: "https://gateway.example.com/auth/oauth2/token", jwksUrl: "https://gateway.example.com/auth/oauth2/jwks", logoutUrl: "https://gateway.example.com/auth/token/logout", clientId: "customPC", clientSecret: "client-secret", scope: "server", issuer: "https://pig4cloud.com", sessionSecret: "test-session-secret-with-enough-entropy", clockSkewSeconds: 60 }; describe("SSO auth helpers", () => { afterEach(() => { vi.unstubAllGlobals(); clearJwksCacheForTests(); }); it("round-trips signed session cookies and rejects tampering or expiry", async () => { const session: AuthSession = { version: 1, issuedAt: 100, expiresAt: 200, user: { id: "auth:customPC:1", subject: "zhangsan", username: "zhangsan", displayName: "张三", clientId: "customPC", authorities: ["ROLE_1"], scope: ["server"] } }; const cookie = await createSessionCookieValue(session, authConfig.sessionSecret || ""); expect(await parseSessionCookieValue(cookie, authConfig.sessionSecret || "", 150)).toMatchObject({ user: { id: "auth:customPC:1", displayName: "张三" } }); expect(await parseSessionCookieValue(`${cookie.slice(0, -1)}x`, authConfig.sessionSecret || "", 150)).toBeNull(); expect(await parseSessionCookieValue(cookie, authConfig.sessionSecret || "", 201)).toBeNull(); }); it("verifies RS256 JWTs from JWKS and maps stable owner ids", async () => { const { publicKey, privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 }); const jwk = publicKey.export({ format: "jwk" }) as TestJwk; jwk.kid = "kid-1"; jwk.alg = "RS256"; jwk.use = "sig"; vi.stubGlobal("fetch", async () => new Response(JSON.stringify({ keys: [jwk] }), { status: 200 })); const token = signJwt({ iss: authConfig.issuer, sub: "zhangsan", exp: Math.floor(Date.now() / 1000) + 600, iat: Math.floor(Date.now() / 1000) - 10, nbf: Math.floor(Date.now() / 1000) - 10, scope: "server", client_id: "customPC", clientId: "customPC", user_id: 1, username: "zhangsan", tenant_id: 2, authorities: ["ROLE_1", "sys_user_view"] }, privateKey, "kid-1"); const claims = await verifyAuthJwt(token, authConfig); expect(userFromClaims(claims, authConfig)).toMatchObject({ id: "auth:customPC:1", displayName: "zhangsan", tenantId: "2", authorities: ["ROLE_1", "sys_user_view"], scope: ["server"] }); }); it("rejects JWTs for another OAuth client", async () => { const { publicKey, privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 }); const jwk = publicKey.export({ format: "jwk" }) as TestJwk; jwk.kid = "kid-2"; vi.stubGlobal("fetch", async () => new Response(JSON.stringify({ keys: [jwk] }), { status: 200 })); const token = signJwt({ iss: authConfig.issuer, exp: Math.floor(Date.now() / 1000) + 600, scope: "server", client_id: "other-client", sub: "zhangsan" }, privateKey, "kid-2"); await expect(verifyAuthJwt(token, authConfig)).rejects.toThrow("client id"); }); }); function signJwt(payload: Record, privateKey: KeyObject, kid: string): string { const header = base64UrlJson({ alg: "RS256", typ: "JWT", kid }); const body = base64UrlJson(payload); const signingInput = `${header}.${body}`; const signer = createSign("RSA-SHA256"); signer.update(signingInput); signer.end(); const signature = signer.sign(privateKey).toString("base64url"); return `${signingInput}.${signature}`; } function base64UrlJson(value: unknown): string { return Buffer.from(JSON.stringify(value)).toString("base64url"); }