diff --git a/README.md b/README.md index 03012f56..38453c19 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Currently only Cloudflare Workers are supported officially. However, it may work ## Synopsis +### Module Worker Syntax (recommend) + ```ts import { Hono } from "hono"; import { VerifyFirebaseAuthConfig, VerifyFirebaseAuthEnv, verifyFirebaseAuth, getFirebaseToken } from "@honojs/firebase-auth"; @@ -28,6 +30,37 @@ app.get("/hello", (c) => { export default app ``` +### Service Worker Syntax + +```ts +import { Hono } from "hono"; +import { VerifyFirebaseAuthConfig, verifyFirebaseAuth, getFirebaseToken } from "@honojs/firebase-auth"; + +const config: VerifyFirebaseAuthConfig = { + // specify your firebase project ID. + projectId: "your-project-id", + // this is optional. but required in this mode. + keyStore: WorkersKVStoreSingle.getOrInitialize( + PUBLIC_JWK_CACHE_KEY, + PUBLIC_JWK_CACHE_KV + ), + // this is also optional. But in this mode, you can only specify here. + firebaseEmulatorHost: FIREBASE_AUTH_EMULATOR_HOST, +} + +const app = new Hono() + +// set middleware +app.use("*", verifyFirebaseAuth(config)); +app.get("/hello", (c) => { + const idToken = getFirebaseToken(c) // get id-token object. + return c.json(idToken) +}); + +app.fire() +``` + + ## Config (`VerifyFirebaseAuthConfig`) ### `projectId: string` (**required**) @@ -54,6 +87,12 @@ If you don't specify the field, this library uses [WorkersKVStoreSingle](https:/ Throws an exception if JWT validation fails. By default, this is output to the error log, but if you don't expect it, use this. +### `firebaseEmulatorHost?: string` (optional) + +You can specify a host for the Firebase Auth emulator. This config is mainly used when **Service Worker Syntax** is used. + +If not specified, check the [`FIREBASE_AUTH_EMULATOR_HOST` environment variable obtained from the request](https://github.com/Code-Hex/firebase-auth-cloudflare-workers#emulatorenv). + ## Author codehex diff --git a/src/index.ts b/src/index.ts index 41ccf787..a5036dd5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,7 @@ export interface VerifyFirebaseAuthConfig { keyStore?: KeyStorer; keyStoreInitializer?: (c: Context) => KeyStorer; disableErrorLog?: boolean; + firebaseEmulatorHost?: string; } const defaultKVStoreJWKCacheKey = "verify-firebase-auth-cached-public-key"; @@ -30,7 +31,7 @@ const defaultKeyStoreInitializer = (c: Context): KeyStorer => { export const verifyFirebaseAuth = ( userConfig: VerifyFirebaseAuthConfig -): Handler => { +): Handler => { const config = { projectId: userConfig.projectId, AuthorizationHeaderKey: @@ -39,6 +40,7 @@ export const verifyFirebaseAuth = ( keyStoreInitializer: userConfig.keyStoreInitializer ?? defaultKeyStoreInitializer, disableErrorLog: userConfig.disableErrorLog, + firebaseEmulatorHost: userConfig.firebaseEmulatorHost, }; return async (c, next) => { @@ -55,7 +57,10 @@ export const verifyFirebaseAuth = ( ); try { - const idToken = await auth.verifyIdToken(jwt, c.env); + const idToken = await auth.verifyIdToken(jwt, { + FIREBASE_AUTH_EMULATOR_HOST: + config.firebaseEmulatorHost ?? c.env.FIREBASE_AUTH_EMULATOR_HOST, + }); setFirebaseToken(c, idToken); } catch (err) { if (!userConfig.disableErrorLog) { diff --git a/test/index.test.ts b/test/index.test.ts index c8488c1c..fc6eb0d6 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,4 +1,8 @@ -import { Auth, KeyStorer } from "firebase-auth-cloudflare-workers"; +import { + Auth, + KeyStorer, + WorkersKVStoreSingle, +} from "firebase-auth-cloudflare-workers"; import { Hono } from "hono"; import { VerifyFirebaseAuthEnv, @@ -26,167 +30,209 @@ describe("verifyFirebaseAuth middleware", () => { await sleep(1000); // wait for iat }); - test.each([ - [ - "valid case, should be 200", - { - headerKey: "Authorization", - env: { - FIREBASE_AUTH_EMULATOR_HOST: "localhost:9099", - PUBLIC_JWK_CACHE_KEY: "testing-cache-key", - PUBLIC_JWK_CACHE_KV, - }, - config: { + describe("service worker syntax", () => { + test("valid case, should be 200", async () => { + const app = new Hono(); + + resetAuth(); + + // This is assumed to be obtained from an environment variable. + const PUBLIC_JWK_CACHE_KEY = "testing-cache-key"; + + app.use( + "*", + verifyFirebaseAuth({ projectId: validProjectId, - }, - wantStatus: 200, - }, - ], - [ - "valid specified headerKey, should be 200", - { - headerKey: "X-Authorization", - env: { - FIREBASE_AUTH_EMULATOR_HOST: "localhost:9099", - PUBLIC_JWK_CACHE_KEY: "testing-cache-key", - PUBLIC_JWK_CACHE_KV, - }, - config: { - projectId: validProjectId, - authorizationHeaderKey: "X-Authorization", - }, - wantStatus: 200, - }, - ], - [ - "invalid authorization header, should be 400", - { - headerKey: "X-Authorization", - env: { - FIREBASE_AUTH_EMULATOR_HOST: "localhost:9099", - PUBLIC_JWK_CACHE_KEY: "testing-cache-key", - PUBLIC_JWK_CACHE_KV, - }, - config: { - projectId: validProjectId, // see package.json - // No specified header key. - }, - wantStatus: 400, - }, - ], - [ - "invalid project ID, should be 401", - { - headerKey: "Authorization", - env: { - FIREBASE_AUTH_EMULATOR_HOST: "localhost:9099", - PUBLIC_JWK_CACHE_KEY: "testing-cache-key", - PUBLIC_JWK_CACHE_KV, - }, - config: { - projectId: "invalid-projectId", - }, - wantStatus: 401, - }, - ], - ])("%s", async (_, { headerKey, env, config, wantStatus }) => { - const app = new Hono(); + keyStore: WorkersKVStoreSingle.getOrInitialize( + PUBLIC_JWK_CACHE_KEY, + PUBLIC_JWK_CACHE_KV + ), + disableErrorLog: true, + firebaseEmulatorHost: emulatorHost, + }) + ); + app.get("/hello", (c) => c.json(getFirebaseToken(c))); - resetAuth(); + const req = new Request("http://localhost/hello", { + headers: { + Authorization: `Bearer ${user.idToken}`, + }, + }); - app.use( - "*", - verifyFirebaseAuth({ - ...config, - disableErrorLog: true, - }) - ); - app.get("/hello", (c) => c.text("OK")); + const res = await app.request(req); - const req = new Request("http://localhost/hello", { - headers: { - [headerKey]: `Bearer ${user.idToken}`, - }, + expect(res).not.toBeNull(); + expect(res.status).toBe(200); + + const json = await res.json<{ aud: string; email: string }>(); + expect(json.aud).toBe(validProjectId); + expect(json.email).toBe("codehex@hono.js"); }); - - const res = await app.fetch(req, env); - - expect(res).not.toBeNull(); - expect(res.status).toBe(wantStatus); }); - test("specified keyStore is used", async () => { - const testingJWT = generateDummyJWT(); + describe("module worker syntax", () => { + test.each([ + [ + "valid case, should be 200", + { + headerKey: "Authorization", + env: { + FIREBASE_AUTH_EMULATOR_HOST: "localhost:9099", + PUBLIC_JWK_CACHE_KEY: "testing-cache-key", + PUBLIC_JWK_CACHE_KV, + }, + config: { + projectId: validProjectId, + }, + wantStatus: 200, + }, + ], + [ + "valid specified headerKey, should be 200", + { + headerKey: "X-Authorization", + env: { + FIREBASE_AUTH_EMULATOR_HOST: "localhost:9099", + PUBLIC_JWK_CACHE_KEY: "testing-cache-key", + PUBLIC_JWK_CACHE_KV, + }, + config: { + projectId: validProjectId, + authorizationHeaderKey: "X-Authorization", + }, + wantStatus: 200, + }, + ], + [ + "invalid authorization header, should be 400", + { + headerKey: "X-Authorization", + env: { + FIREBASE_AUTH_EMULATOR_HOST: "localhost:9099", + PUBLIC_JWK_CACHE_KEY: "testing-cache-key", + PUBLIC_JWK_CACHE_KV, + }, + config: { + projectId: validProjectId, // see package.json + // No specified header key. + }, + wantStatus: 400, + }, + ], + [ + "invalid project ID, should be 401", + { + headerKey: "Authorization", + env: { + FIREBASE_AUTH_EMULATOR_HOST: "localhost:9099", + PUBLIC_JWK_CACHE_KEY: "testing-cache-key", + PUBLIC_JWK_CACHE_KV, + }, + config: { + projectId: "invalid-projectId", + }, + wantStatus: 401, + }, + ], + ])("%s", async (_, { headerKey, env, config, wantStatus }) => { + const app = new Hono(); - const nopKeyStore = new NopKeyStore(); - const getSpy = jest.spyOn(nopKeyStore, "get"); - const putSpy = jest.spyOn(nopKeyStore, "put"); + resetAuth(); - const app = new Hono(); + app.use( + "*", + verifyFirebaseAuth({ + ...config, + disableErrorLog: true, + }) + ); + app.get("/hello", (c) => c.text("OK")); - resetAuth(); + const req = new Request("http://localhost/hello", { + headers: { + [headerKey]: `Bearer ${user.idToken}`, + }, + }); - app.use( - "*", - verifyFirebaseAuth({ - projectId: validProjectId, - keyStore: nopKeyStore, - disableErrorLog: true, - }) - ); - app.get("/hello", (c) => c.text("OK")); + const res = await app.fetch(req, env); - const req = new Request("http://localhost/hello", { - headers: { - Authorization: `Bearer ${testingJWT}`, - }, + expect(res).not.toBeNull(); + expect(res.status).toBe(wantStatus); }); - // not use firebase emulator to check using key store - const res = await app.fetch(req, { - FIREBASE_AUTH_EMULATOR_HOST: undefined, + test("specified keyStore is used", async () => { + const testingJWT = generateDummyJWT(); + + const nopKeyStore = new NopKeyStore(); + const getSpy = jest.spyOn(nopKeyStore, "get"); + const putSpy = jest.spyOn(nopKeyStore, "put"); + + const app = new Hono(); + + resetAuth(); + + app.use( + "*", + verifyFirebaseAuth({ + projectId: validProjectId, + keyStore: nopKeyStore, + disableErrorLog: true, + }) + ); + app.get("/hello", (c) => c.text("OK")); + + const req = new Request("http://localhost/hello", { + headers: { + Authorization: `Bearer ${testingJWT}`, + }, + }); + + // not use firebase emulator to check using key store + const res = await app.fetch(req, { + FIREBASE_AUTH_EMULATOR_HOST: undefined, + }); + + expect(res).not.toBeNull(); + expect(res.status).toBe(401); + expect(getSpy).toHaveBeenCalled(); + expect(putSpy).toHaveBeenCalled(); }); - expect(res).not.toBeNull(); - expect(res.status).toBe(401); - expect(getSpy).toHaveBeenCalled(); - expect(putSpy).toHaveBeenCalled(); - }); + test("usable id-token in main handler", async () => { + const testingJWT = generateDummyJWT(); - test("usable id-token in main handler", async () => { - const testingJWT = generateDummyJWT(); + const nopKeyStore = new NopKeyStore(); + const app = new Hono(); - const nopKeyStore = new NopKeyStore(); - const app = new Hono(); + resetAuth(); - resetAuth(); + app.use( + "*", + verifyFirebaseAuth({ + projectId: validProjectId, + keyStore: nopKeyStore, + disableErrorLog: true, + }) + ); + app.get("/hello", (c) => c.json(getFirebaseToken(c))); - app.use( - "*", - verifyFirebaseAuth({ - projectId: validProjectId, - keyStore: nopKeyStore, - disableErrorLog: true, - }) - ); - app.get("/hello", (c) => c.json(getFirebaseToken(c))); + const req = new Request("http://localhost/hello", { + headers: { + Authorization: `Bearer ${testingJWT}`, + }, + }); - const req = new Request("http://localhost/hello", { - headers: { - Authorization: `Bearer ${testingJWT}`, - }, + const res = await app.fetch(req, { + FIREBASE_AUTH_EMULATOR_HOST: emulatorHost, + }); + + expect(res).not.toBeNull(); + expect(res.status).toBe(200); + + const json = await res.json<{ aud: string; email: string }>(); + expect(json.aud).toBe(validProjectId); + expect(json.email).toBe("codehex@hono.js"); }); - - const res = await app.fetch(req, { - FIREBASE_AUTH_EMULATOR_HOST: emulatorHost, - }); - - expect(res).not.toBeNull(); - expect(res.status).toBe(200); - - const json = await res.json<{ aud: string; email: string }>(); - expect(json.aud).toBe(validProjectId); - expect(json.email).toBe("codehex@example.com"); }); }); @@ -220,11 +266,11 @@ const generateDummyJWT = () => { sub: "t1aLdTkAs0S0J0P6TNbjwbmry5B3", iat: now - 1000, exp: now + 3000, // + 3s - email: "codehex@example.com", + email: "codehex@hono.js", email_verified: false, firebase: { identities: { - email: ["codehex@example.com"], + email: ["codehex@hono.js"], }, sign_in_provider: "password", },