Merge pull request #8 from honojs/fix/support-service-worker-syntax

pull/34/head
Kei Kamikawa 2022-07-30 20:24:31 +09:00 committed by GitHub
commit 4cb4e8d957
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 234 additions and 144 deletions

View File

@ -6,6 +6,8 @@ Currently only Cloudflare Workers are supported officially. However, it may work
## Synopsis ## Synopsis
### Module Worker Syntax (recommend)
```ts ```ts
import { Hono } from "hono"; import { Hono } from "hono";
import { VerifyFirebaseAuthConfig, VerifyFirebaseAuthEnv, verifyFirebaseAuth, getFirebaseToken } from "@honojs/firebase-auth"; import { VerifyFirebaseAuthConfig, VerifyFirebaseAuthEnv, verifyFirebaseAuth, getFirebaseToken } from "@honojs/firebase-auth";
@ -28,6 +30,37 @@ app.get("/hello", (c) => {
export default app 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`) ## Config (`VerifyFirebaseAuthConfig`)
### `projectId: string` (**required**) ### `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. 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 ## Author
codehex <https://github.com/Code-Hex> codehex <https://github.com/Code-Hex>

View File

@ -18,6 +18,7 @@ export interface VerifyFirebaseAuthConfig {
keyStore?: KeyStorer; keyStore?: KeyStorer;
keyStoreInitializer?: (c: Context) => KeyStorer; keyStoreInitializer?: (c: Context) => KeyStorer;
disableErrorLog?: boolean; disableErrorLog?: boolean;
firebaseEmulatorHost?: string;
} }
const defaultKVStoreJWKCacheKey = "verify-firebase-auth-cached-public-key"; const defaultKVStoreJWKCacheKey = "verify-firebase-auth-cached-public-key";
@ -30,7 +31,7 @@ const defaultKeyStoreInitializer = (c: Context): KeyStorer => {
export const verifyFirebaseAuth = ( export const verifyFirebaseAuth = (
userConfig: VerifyFirebaseAuthConfig userConfig: VerifyFirebaseAuthConfig
): Handler<string, VerifyFirebaseAuthEnv> => { ): Handler => {
const config = { const config = {
projectId: userConfig.projectId, projectId: userConfig.projectId,
AuthorizationHeaderKey: AuthorizationHeaderKey:
@ -39,6 +40,7 @@ export const verifyFirebaseAuth = (
keyStoreInitializer: keyStoreInitializer:
userConfig.keyStoreInitializer ?? defaultKeyStoreInitializer, userConfig.keyStoreInitializer ?? defaultKeyStoreInitializer,
disableErrorLog: userConfig.disableErrorLog, disableErrorLog: userConfig.disableErrorLog,
firebaseEmulatorHost: userConfig.firebaseEmulatorHost,
}; };
return async (c, next) => { return async (c, next) => {
@ -55,7 +57,10 @@ export const verifyFirebaseAuth = (
); );
try { 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); setFirebaseToken(c, idToken);
} catch (err) { } catch (err) {
if (!userConfig.disableErrorLog) { if (!userConfig.disableErrorLog) {

View File

@ -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 { Hono } from "hono";
import { import {
VerifyFirebaseAuthEnv, VerifyFirebaseAuthEnv,
@ -26,167 +30,209 @@ describe("verifyFirebaseAuth middleware", () => {
await sleep(1000); // wait for iat await sleep(1000); // wait for iat
}); });
test.each([ describe("service worker syntax", () => {
[ test("valid case, should be 200", async () => {
"valid case, should be 200", const app = new Hono();
{
headerKey: "Authorization", resetAuth();
env: {
FIREBASE_AUTH_EMULATOR_HOST: "localhost:9099", // This is assumed to be obtained from an environment variable.
PUBLIC_JWK_CACHE_KEY: "testing-cache-key", const PUBLIC_JWK_CACHE_KEY = "testing-cache-key";
PUBLIC_JWK_CACHE_KV,
}, app.use(
config: { "*",
verifyFirebaseAuth({
projectId: validProjectId, projectId: validProjectId,
}, keyStore: WorkersKVStoreSingle.getOrInitialize(
wantStatus: 200, PUBLIC_JWK_CACHE_KEY,
}, PUBLIC_JWK_CACHE_KV
], ),
[ disableErrorLog: true,
"valid specified headerKey, should be 200", firebaseEmulatorHost: emulatorHost,
{ })
headerKey: "X-Authorization", );
env: { app.get("/hello", (c) => c.json(getFirebaseToken(c)));
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<VerifyFirebaseAuthEnv>();
resetAuth(); const req = new Request("http://localhost/hello", {
headers: {
Authorization: `Bearer ${user.idToken}`,
},
});
app.use( const res = await app.request(req);
"*",
verifyFirebaseAuth({
...config,
disableErrorLog: true,
})
);
app.get("/hello", (c) => c.text("OK"));
const req = new Request("http://localhost/hello", { expect(res).not.toBeNull();
headers: { expect(res.status).toBe(200);
[headerKey]: `Bearer ${user.idToken}`,
}, 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 () => { describe("module worker syntax", () => {
const testingJWT = generateDummyJWT(); 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<VerifyFirebaseAuthEnv>();
const nopKeyStore = new NopKeyStore(); resetAuth();
const getSpy = jest.spyOn(nopKeyStore, "get");
const putSpy = jest.spyOn(nopKeyStore, "put");
const app = new Hono<VerifyFirebaseAuthEnv>(); 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( const res = await app.fetch(req, env);
"*",
verifyFirebaseAuth({
projectId: validProjectId,
keyStore: nopKeyStore,
disableErrorLog: true,
})
);
app.get("/hello", (c) => c.text("OK"));
const req = new Request("http://localhost/hello", { expect(res).not.toBeNull();
headers: { expect(res.status).toBe(wantStatus);
Authorization: `Bearer ${testingJWT}`,
},
}); });
// not use firebase emulator to check using key store test("specified keyStore is used", async () => {
const res = await app.fetch(req, { const testingJWT = generateDummyJWT();
FIREBASE_AUTH_EMULATOR_HOST: undefined,
const nopKeyStore = new NopKeyStore();
const getSpy = jest.spyOn(nopKeyStore, "get");
const putSpy = jest.spyOn(nopKeyStore, "put");
const app = new Hono<VerifyFirebaseAuthEnv>();
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(); test("usable id-token in main handler", async () => {
expect(res.status).toBe(401); const testingJWT = generateDummyJWT();
expect(getSpy).toHaveBeenCalled();
expect(putSpy).toHaveBeenCalled();
});
test("usable id-token in main handler", async () => { const nopKeyStore = new NopKeyStore();
const testingJWT = generateDummyJWT(); const app = new Hono<VerifyFirebaseAuthEnv>();
const nopKeyStore = new NopKeyStore(); resetAuth();
const app = new Hono<VerifyFirebaseAuthEnv>();
resetAuth(); app.use(
"*",
verifyFirebaseAuth({
projectId: validProjectId,
keyStore: nopKeyStore,
disableErrorLog: true,
})
);
app.get("/hello", (c) => c.json(getFirebaseToken(c)));
app.use( const req = new Request("http://localhost/hello", {
"*", headers: {
verifyFirebaseAuth({ Authorization: `Bearer ${testingJWT}`,
projectId: validProjectId, },
keyStore: nopKeyStore, });
disableErrorLog: true,
})
);
app.get("/hello", (c) => c.json(getFirebaseToken(c)));
const req = new Request("http://localhost/hello", { const res = await app.fetch(req, {
headers: { FIREBASE_AUTH_EMULATOR_HOST: emulatorHost,
Authorization: `Bearer ${testingJWT}`, });
},
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", sub: "t1aLdTkAs0S0J0P6TNbjwbmry5B3",
iat: now - 1000, iat: now - 1000,
exp: now + 3000, // + 3s exp: now + 3000, // + 3s
email: "codehex@example.com", email: "codehex@hono.js",
email_verified: false, email_verified: false,
firebase: { firebase: {
identities: { identities: {
email: ["codehex@example.com"], email: ["codehex@hono.js"],
}, },
sign_in_provider: "password", sign_in_provider: "password",
}, },