From 05238849e735135d12a12d276b06d49ccfcd4152 Mon Sep 17 00:00:00 2001 From: Yuchen Shi Date: Tue, 22 Nov 2022 13:13:29 -0800 Subject: [PATCH] Support x-goog-api-key in Auth Emulator. Fix #5249. --- src/emulator/auth/server.ts | 28 ++++++++++++++++++++-------- src/test/emulators/auth/rest.spec.ts | 28 +++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/emulator/auth/server.ts b/src/emulator/auth/server.ts index e63eef89086..f7c61b6c9d2 100644 --- a/src/emulator/auth/server.ts +++ b/src/emulator/auth/server.ts @@ -166,14 +166,25 @@ export async function createApp( ); const apiKeyAuthenticator: PromiseAuthenticator = (ctx, info) => { - if (info.in !== "query") { - throw new Error('apiKey must be defined as in: "query" in API spec.'); - } if (!info.name) { - throw new Error("apiKey param name is undefined in API spec."); + throw new Error("apiKey param/header name is undefined in API spec."); + } + + let key: string | undefined; + const req = ctx.req as express.Request; + switch (info.in) { + case "header": + key = req.get(info.name); + break; + case "query": { + const q = req.query[info.name]; + key = typeof q === "string" ? q : undefined; + break; + } + default: + throw new Error('apiKey must be defined as in: "query" or "header" in API spec.'); } - const key = (ctx.req as express.Request).query[info.name]; - if (typeof key === "string" && key.length > 0) { + if (key) { return { type: "success", user: getProjectIdByApiKey(key) }; } else { return undefined; @@ -218,7 +229,8 @@ export async function createApp( const apis = await exegesisExpress.middleware(specForRouter(), { controllers: { auth: toExegesisController(authOperations, getProjectStateById) }, authenticators: { - apiKey: apiKeyAuthenticator, + apiKeyQuery: apiKeyAuthenticator, + apiKeyHeader: apiKeyAuthenticator, Oauth2: oauth2Authenticator, }, autoHandleHttpErrors(err) { @@ -305,7 +317,7 @@ export async function createApp( if (ctx.res.statusCode === 401) { // Normalize unauthenticated responses to match production. const requirements = (ctx.api.operationObject as OperationObject).security; - if (requirements?.some((req) => req.apiKey)) { + if (requirements?.some((req) => req.apiKeyQuery || req.apiKeyHeader)) { throw new PermissionDeniedError("The request is missing a valid API key."); } else { throw new UnauthenticatedError( diff --git a/src/test/emulators/auth/rest.spec.ts b/src/test/emulators/auth/rest.spec.ts index b2adb5efeb4..8e2494e968f 100644 --- a/src/test/emulators/auth/rest.spec.ts +++ b/src/test/emulators/auth/rest.spec.ts @@ -93,6 +93,28 @@ describeAuthEmulator("authentication", ({ authApi }) => { }); }); + it("should accept API key as a query parameter", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .query({ key: "fake-api-key" }) + .send({}) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).not.to.have.property("error"); + }); + }); + + it("should accept API key in HTTP Header x-goog-api-key", async () => { + await authApi() + .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") + .set("x-goog-api-key", "fake-api-key") + .send({}) + .then((res) => { + expectStatusCode(200, res); + expect(res.body).not.to.have.property("error"); + }); + }); + it("should ignore non-Bearer Authorization headers", async () => { await authApi() .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") @@ -143,7 +165,11 @@ describeAuthEmulator("authentication", ({ authApi }) => { .post("/identitytoolkit.googleapis.com/v1/accounts:signUp") // This authenticates as owner of the default projectId. The exact value // and expiry don't matter -- the Emulator only checks for the format. - .set("Authorization", "Bearer ya29.AHES6ZRVmB7fkLtd1XTmq6mo0S1wqZZi3-Lh_s-6Uw7p8vtgSwg") + .set( + "Authorization", + // Not an actual token. Breaking it down to avoid linter false positives. + "Bearer ya" + "29.AHES0ZZZZZ0fff" + "ff0XXXX0mmmm0wwwww0-LL_l-0bb0b0bbbbbb" + ) .send({ // This field requires OAuth 2 and should work correctly. targetProjectId: "example2",