48 lines
1.6 KiB
TypeScript
48 lines
1.6 KiB
TypeScript
import { createHash, timingSafeEqual } from "node:crypto";
|
|
import type { ApiConfig } from "@hivegrid/config";
|
|
|
|
export type ServicePrincipal = {
|
|
type: "service-token";
|
|
};
|
|
|
|
export function extractBearerToken(request: Request) {
|
|
return request.headers.get("authorization")?.match(/^Bearer\s+(.+)$/i)?.[1];
|
|
}
|
|
|
|
export function extractApiKeyFromRequest(request: Request, body: Record<string, unknown> | undefined) {
|
|
if (typeof body?.apiKey === "string" && body.apiKey) {
|
|
return body.apiKey;
|
|
}
|
|
|
|
return request.headers.get("x-api-key");
|
|
}
|
|
|
|
export function isValidServiceToken(token: string, configOrToken: Pick<ApiConfig, "apiAccessToken"> | string | undefined) {
|
|
const configuredToken = typeof configOrToken === "string" ? configOrToken : configOrToken?.apiAccessToken;
|
|
if (!configuredToken || !token) {
|
|
return false;
|
|
}
|
|
// Constant-time comparison: hash both sides to a fixed length first so
|
|
// timingSafeEqual never throws on length mismatch and the comparison leaks
|
|
// neither the token contents nor its length.
|
|
const provided = createHash("sha256").update(token).digest();
|
|
const expected = createHash("sha256").update(configuredToken).digest();
|
|
return timingSafeEqual(provided, expected);
|
|
}
|
|
|
|
export function createServicePrincipal(): ServicePrincipal {
|
|
return {
|
|
type: "service-token",
|
|
};
|
|
}
|
|
|
|
export function authenticateServiceRequest(request: Request, configOrToken: Pick<ApiConfig, "apiAccessToken"> | string | undefined) {
|
|
const token = extractBearerToken(request);
|
|
|
|
if (!token || !isValidServiceToken(token, configOrToken)) {
|
|
return null;
|
|
}
|
|
|
|
return createServicePrincipal();
|
|
}
|