Add authenticated login and SSO protection
This commit is contained in:
124
tests/auth-session.test.ts
Normal file
124
tests/auth-session.test.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
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<string, unknown>, 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");
|
||||
}
|
||||
Reference in New Issue
Block a user