修改认证中心对接方式
This commit is contained in:
97
tests/auth-password-route.test.ts
Normal file
97
tests/auth-password-route.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { createSign, generateKeyPairSync, type KeyObject } from "node:crypto";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { clearJwksCacheForTests } from "@/lib/server/auth/jwt";
|
||||
import { POST } from "@/app/api/auth/password/route";
|
||||
|
||||
type TestJwk = JsonWebKey & {
|
||||
kid?: string;
|
||||
alg?: string;
|
||||
use?: string;
|
||||
};
|
||||
|
||||
const baseEnv = {
|
||||
ZHINIAN_AUTH_REQUIRED: "1",
|
||||
ZHINIAN_AUTH_BASE_URL: "https://gateway.example.com/auth",
|
||||
ZHINIAN_AUTH_CLIENT_ID: "agentbus-client",
|
||||
ZHINIAN_AUTH_CLIENT_SECRET: "client-secret",
|
||||
ZHINIAN_AUTH_SCOPE: "server",
|
||||
ZHINIAN_AUTH_ISSUER: "https://pig4cloud.com",
|
||||
ZHINIAN_AUTH_SESSION_SECRET: "test-session-secret-with-enough-entropy"
|
||||
};
|
||||
|
||||
describe("password auth route AgentBus compatibility", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.unstubAllGlobals();
|
||||
clearJwksCacheForTests();
|
||||
});
|
||||
|
||||
it("encrypts password with the configured AES-CFB key and does not require captcha fields", async () => {
|
||||
for (const [key, value] of Object.entries(baseEnv)) vi.stubEnv(key, value);
|
||||
vi.stubEnv("ZHINIAN_AUTH_PASSWORD_ENC_KEY", "thanks,pig4cloud");
|
||||
const { publicKey, privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
|
||||
const jwk = publicKey.export({ format: "jwk" }) as TestJwk;
|
||||
jwk.kid = "agentbus-key";
|
||||
const accessToken = signJwt({
|
||||
iss: baseEnv.ZHINIAN_AUTH_ISSUER,
|
||||
sub: "subject-42",
|
||||
user_id: "remote-42",
|
||||
username: "user@example.com",
|
||||
client_id: baseEnv.ZHINIAN_AUTH_CLIENT_ID,
|
||||
scope: baseEnv.ZHINIAN_AUTH_SCOPE,
|
||||
exp: Math.floor(Date.now() / 1000) + 600,
|
||||
iat: Math.floor(Date.now() / 1000) - 10,
|
||||
nbf: Math.floor(Date.now() / 1000) - 10
|
||||
}, privateKey, "agentbus-key");
|
||||
const seenBodies: URLSearchParams[] = [];
|
||||
|
||||
vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
if (url.endsWith("/oauth2/jwks")) {
|
||||
return new Response(JSON.stringify({ keys: [jwk] }), { status: 200 });
|
||||
}
|
||||
if (url.endsWith("/oauth2/token")) {
|
||||
seenBodies.push(new URLSearchParams(String(init?.body)));
|
||||
return new Response(JSON.stringify({
|
||||
access_token: accessToken,
|
||||
refresh_token: "refresh-token-1",
|
||||
token_type: "bearer",
|
||||
expires_in: "3600"
|
||||
}), { status: 200 });
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
});
|
||||
|
||||
const response = await POST(new Request("https://app.example.com/api/auth/password", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
username: "user@example.com",
|
||||
password: "123456",
|
||||
next: "/create"
|
||||
})
|
||||
}));
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(seenBodies).toHaveLength(1);
|
||||
expect(seenBodies[0].get("grant_type")).toBe("password");
|
||||
expect(seenBodies[0].get("username")).toBe("user@example.com");
|
||||
expect(seenBodies[0].get("password")).toBe("YehdBPev");
|
||||
expect(seenBodies[0].has("code")).toBe(false);
|
||||
expect(seenBodies[0].has("randomStr")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
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");
|
||||
}
|
||||
@@ -106,6 +106,28 @@ describe("SSO auth helpers", () => {
|
||||
|
||||
await expect(verifyAuthJwt(token, authConfig)).rejects.toThrow("client id");
|
||||
});
|
||||
|
||||
it("accepts JWTs without optional client and scope claims like AgentBus", async () => {
|
||||
const { publicKey, privateKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
|
||||
const jwk = publicKey.export({ format: "jwk" }) as TestJwk;
|
||||
jwk.kid = "kid-3";
|
||||
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,
|
||||
user_id: 1,
|
||||
username: "zhangsan"
|
||||
}, privateKey, "kid-3");
|
||||
|
||||
await expect(verifyAuthJwt(token, authConfig)).resolves.toMatchObject({
|
||||
sub: "zhangsan",
|
||||
user_id: 1
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function signJwt(payload: Record<string, unknown>, privateKey: KeyObject, kid: string): string {
|
||||
|
||||
@@ -2,19 +2,26 @@ import { describe, expect, it } from "vitest";
|
||||
import vm from "node:vm";
|
||||
import { randomUUIDPolyfillScript } from "@/lib/client/random-uuid-polyfill";
|
||||
|
||||
type RandomUUIDContext = {
|
||||
crypto?: {
|
||||
randomUUID?: () => string;
|
||||
getRandomValues?: (bytes: Uint8Array) => Uint8Array;
|
||||
};
|
||||
};
|
||||
|
||||
describe("client randomUUID polyfill", () => {
|
||||
it("defines crypto.randomUUID when crypto is missing", () => {
|
||||
const context = {};
|
||||
const context: RandomUUIDContext = {};
|
||||
|
||||
vm.runInNewContext(randomUUIDPolyfillScript, context);
|
||||
|
||||
expect(context.crypto.randomUUID()).toMatch(
|
||||
expect(context.crypto?.randomUUID?.()).toMatch(
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
|
||||
);
|
||||
});
|
||||
|
||||
it("defines crypto.randomUUID when only getRandomValues is available", () => {
|
||||
const context = {
|
||||
const context: RandomUUIDContext = {
|
||||
crypto: {
|
||||
getRandomValues(bytes: Uint8Array) {
|
||||
for (let index = 0; index < bytes.length; index += 1) bytes[index] = index;
|
||||
@@ -25,7 +32,7 @@ describe("client randomUUID polyfill", () => {
|
||||
|
||||
vm.runInNewContext(randomUUIDPolyfillScript, context);
|
||||
|
||||
expect(context.crypto.randomUUID()).toMatch(
|
||||
expect(context.crypto?.randomUUID?.()).toMatch(
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user