import { SSMClient, GetParameterCommand, } from "@aws-sdk/client-ssm"; import { timingSafeEqual } from "node:crypto"; const ssm = new SSMClient({}); function tokensMatch(provided: string, expected: string): boolean { const a = Buffer.from(provided); const b = Buffer.from(expected); // timingSafeEqual throws on unequal-length buffers; check length first. return a.length === b.length && timingSafeEqual(a, b); } let cachedAuthToken: string | undefined; async function getAuthToken(): Promise { if (cachedAuthToken) return cachedAuthToken; const res = await ssm.send( new GetParameterCommand({ Name: process.env.AUTH_TOKEN_PARAM!, WithDecryption: true, }) ); cachedAuthToken = res.Parameter!.Value!; return cachedAuthToken; } /** * API Gateway HTTP API (payload v2.0) REQUEST Lambda authorizer. * * Validates the SAME `?token=` query-string parameter the Yealink XML Browser * keys already send (type-17 keys issue a plain GET and cannot send headers or * a POST body), so this authorizer is transparent to the phones. An * unauthenticated or wrong-token request is now rejected at the gateway with * 401/403 before any handler Lambda is invoked. * * Returns the simple-response shape ({ isAuthorized }) which the routes are * configured for (enableSimpleResponses: true). */ export async function handler(event: { queryStringParameters?: Record; }): Promise<{ isAuthorized: boolean }> { const token = event.queryStringParameters?.token; if (!token) { return { isAuthorized: false }; } let expected: string; try { expected = await getAuthToken(); } catch (err) { console.error( JSON.stringify({ action: "authorize", status: "error", reason: "ssm_failure", error: String(err) }) ); // Fail closed: deny if the token cannot be loaded. return { isAuthorized: false }; } return { isAuthorized: tokensMatch(token, expected) }; }