新增案例库浏览器登录态
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { requireApiAccess } from "../auth";
|
||||
import {
|
||||
apiSessionCookieName,
|
||||
createApiSessionToken,
|
||||
requireApiAccess,
|
||||
} from "../auth";
|
||||
|
||||
describe("requireApiAccess", () => {
|
||||
test("allows local test requests when auth is explicitly disabled", () => {
|
||||
@@ -53,4 +57,32 @@ describe("requireApiAccess", () => {
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
test("allows requests with a valid HttpOnly session cookie", () => {
|
||||
const request = new Request("http://localhost/api/jobs", {
|
||||
headers: {
|
||||
cookie: `${apiSessionCookieName}=${createApiSessionToken("secret")}`,
|
||||
},
|
||||
});
|
||||
|
||||
const result = requireApiAccess(request, {
|
||||
apiAccessKey: "secret",
|
||||
authDisabled: false,
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
test("rejects requests with an invalid session cookie", () => {
|
||||
const request = new Request("http://localhost/api/jobs", {
|
||||
headers: { cookie: `${apiSessionCookieName}=wrong` },
|
||||
});
|
||||
|
||||
const result = requireApiAccess(request, {
|
||||
apiAccessKey: "secret",
|
||||
authDisabled: false,
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
+47
-2
@@ -1,7 +1,13 @@
|
||||
import { createHmac, timingSafeEqual } from "node:crypto";
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { getAppCloudflareEnv } from "../runtime/cloudflare";
|
||||
|
||||
export const apiSessionCookieName = "geo_api_session";
|
||||
|
||||
const sessionTokenMessage = "geo-agent-api-session:v1";
|
||||
|
||||
interface ApiAccessOptions {
|
||||
apiAccessKey?: string;
|
||||
authDisabled?: boolean;
|
||||
@@ -33,7 +39,10 @@ export function requireApiAccess(
|
||||
}
|
||||
|
||||
const provided = request.headers.get("x-api-key");
|
||||
if (provided !== options.apiAccessKey) {
|
||||
if (
|
||||
provided !== options.apiAccessKey &&
|
||||
!hasValidApiSession(request, options.apiAccessKey)
|
||||
) {
|
||||
return {
|
||||
ok: false,
|
||||
response: NextResponse.json({ error: "Unauthorized" }, { status: 401 }),
|
||||
@@ -43,6 +52,42 @@ export function requireApiAccess(
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
function getConfiguredApiAccessKey() {
|
||||
export function hasValidApiSession(request: Request, apiAccessKey: string) {
|
||||
const cookieValue = parseCookieHeader(request.headers.get("cookie"))[
|
||||
apiSessionCookieName
|
||||
];
|
||||
if (!cookieValue) return false;
|
||||
return safeEqual(cookieValue, createApiSessionToken(apiAccessKey));
|
||||
}
|
||||
|
||||
export function createApiSessionToken(apiAccessKey: string) {
|
||||
return createHmac("sha256", apiAccessKey)
|
||||
.update(sessionTokenMessage)
|
||||
.digest("hex");
|
||||
}
|
||||
|
||||
export function getConfiguredApiAccessKey() {
|
||||
return getAppCloudflareEnv()?.API_ACCESS_KEY ?? process.env.API_ACCESS_KEY;
|
||||
}
|
||||
|
||||
function parseCookieHeader(value: string | null) {
|
||||
const cookies: Record<string, string> = {};
|
||||
if (!value) return cookies;
|
||||
|
||||
for (const part of value.split(";")) {
|
||||
const separator = part.indexOf("=");
|
||||
if (separator < 0) continue;
|
||||
const name = part.slice(0, separator).trim();
|
||||
const cookieValue = part.slice(separator + 1).trim();
|
||||
if (name) cookies[name] = decodeURIComponent(cookieValue);
|
||||
}
|
||||
|
||||
return cookies;
|
||||
}
|
||||
|
||||
function safeEqual(left: string, right: string) {
|
||||
const leftBuffer = Buffer.from(left);
|
||||
const rightBuffer = Buffer.from(right);
|
||||
if (leftBuffer.length !== rightBuffer.length) return false;
|
||||
return timingSafeEqual(leftBuffer, rightBuffer);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user