honojs-middleware/packages/firebase-auth/src/index.ts

154 lines
4.9 KiB
TypeScript
Raw Normal View History

import { getCookie } from 'hono/cookie'
import type { KeyStorer, FirebaseIdToken } from 'firebase-auth-cloudflare-workers'
2023-02-04 19:20:42 +08:00
import { Auth, WorkersKVStoreSingle } from 'firebase-auth-cloudflare-workers'
import type { Context, MiddlewareHandler } from 'hono'
import { HTTPException } from 'hono/http-exception'
2022-07-23 22:56:20 +08:00
export type VerifyFirebaseAuthEnv = {
2023-02-04 19:20:42 +08:00
PUBLIC_JWK_CACHE_KEY?: string | undefined
PUBLIC_JWK_CACHE_KV?: KVNamespace | undefined
FIREBASE_AUTH_EMULATOR_HOST: string | undefined
2022-07-23 22:56:20 +08:00
}
2022-07-28 13:58:54 +08:00
export interface VerifyFirebaseAuthConfig {
2023-02-04 19:20:42 +08:00
projectId: string
authorizationHeaderKey?: string
keyStore?: KeyStorer
keyStoreInitializer?: (c: Context) => KeyStorer
disableErrorLog?: boolean
firebaseEmulatorHost?: string
2022-07-28 13:58:54 +08:00
}
2023-02-04 19:20:42 +08:00
const defaultKVStoreJWKCacheKey = 'verify-firebase-auth-cached-public-key'
const defaultKeyStoreInitializer = (c: Context<{ Bindings: VerifyFirebaseAuthEnv }>): KeyStorer => {
if (c.env.PUBLIC_JWK_CACHE_KV === undefined) {
const res = new Response('Not Implemented', {
status: 501,
})
throw new HTTPException(res.status, { res })
}
2022-07-28 13:58:54 +08:00
return WorkersKVStoreSingle.getOrInitialize(
c.env.PUBLIC_JWK_CACHE_KEY ?? defaultKVStoreJWKCacheKey,
c.env.PUBLIC_JWK_CACHE_KV
2023-02-04 19:20:42 +08:00
)
}
export const verifyFirebaseAuth = (userConfig: VerifyFirebaseAuthConfig): MiddlewareHandler => {
2022-07-28 13:58:54 +08:00
const config = {
projectId: userConfig.projectId,
authorizationHeaderKey: userConfig.authorizationHeaderKey ?? 'Authorization',
keyStore: userConfig.keyStore,
2023-02-04 19:20:42 +08:00
keyStoreInitializer: userConfig.keyStoreInitializer ?? defaultKeyStoreInitializer,
2022-07-28 13:58:54 +08:00
disableErrorLog: userConfig.disableErrorLog,
2022-07-29 22:03:09 +08:00
firebaseEmulatorHost: userConfig.firebaseEmulatorHost,
} satisfies VerifyFirebaseAuthConfig
// TODO(codehex): will be supported
const checkRevoked = false
2022-07-28 13:58:54 +08:00
return async (c, next) => {
const authorization = c.req.raw.headers.get(config.authorizationHeaderKey)
2022-07-28 13:58:54 +08:00
if (authorization === null) {
const res = new Response('Bad Request', {
2022-07-28 13:58:54 +08:00
status: 400,
2023-02-04 19:20:42 +08:00
})
throw new HTTPException(res.status, { res, message: 'authorization header is empty' })
2022-07-28 13:58:54 +08:00
}
2023-02-04 19:20:42 +08:00
const jwt = authorization.replace(/Bearer\s+/i, '')
2022-07-28 13:58:54 +08:00
const auth = Auth.getOrInitialize(
config.projectId,
config.keyStore ?? config.keyStoreInitializer(c)
2023-02-04 19:20:42 +08:00
)
2022-07-28 13:58:54 +08:00
try {
const idToken = await auth.verifyIdToken(jwt, checkRevoked, {
2022-07-29 22:03:09 +08:00
FIREBASE_AUTH_EMULATOR_HOST:
config.firebaseEmulatorHost ?? c.env.FIREBASE_AUTH_EMULATOR_HOST,
2023-02-04 19:20:42 +08:00
})
setFirebaseToken(c, idToken)
2022-07-28 13:58:54 +08:00
} catch (err) {
if (!userConfig.disableErrorLog) {
console.error({
2023-02-04 19:20:42 +08:00
message: 'failed to verify the requested firebase token',
2022-07-28 13:58:54 +08:00
err,
2023-02-04 19:20:42 +08:00
})
2022-07-28 13:58:54 +08:00
}
const res = new Response('Unauthorized', {
2022-07-28 13:58:54 +08:00
status: 401,
2023-02-04 19:20:42 +08:00
})
throw new HTTPException(res.status, {
res,
message: `failed to verify the requested firebase token: ${String(err)}`,
})
2022-07-28 13:58:54 +08:00
}
2023-02-04 19:20:42 +08:00
await next()
}
}
2022-07-28 13:58:54 +08:00
2023-02-04 19:20:42 +08:00
const idTokenContextKey = 'firebase-auth-cloudflare-id-token-key'
2022-07-28 13:58:54 +08:00
2023-02-04 19:20:42 +08:00
const setFirebaseToken = (c: Context, idToken: FirebaseIdToken) => c.set(idTokenContextKey, idToken)
2022-07-28 13:58:54 +08:00
export const getFirebaseToken = (c: Context): FirebaseIdToken | null => {
2023-02-04 19:20:42 +08:00
const idToken = c.get(idTokenContextKey)
if (!idToken) {
return null
}
2023-02-04 19:20:42 +08:00
return idToken
}
export interface VerifySessionCookieFirebaseAuthConfig {
projectId: string
cookieName?: string
keyStore?: KeyStorer
keyStoreInitializer?: (c: Context) => KeyStorer
firebaseEmulatorHost?: string
redirects: {
signIn: string
}
}
export const verifySessionCookieFirebaseAuth = (
userConfig: VerifySessionCookieFirebaseAuthConfig
): MiddlewareHandler => {
const config = {
projectId: userConfig.projectId,
cookieName: userConfig.cookieName ?? 'session',
keyStore: userConfig.keyStore,
keyStoreInitializer: userConfig.keyStoreInitializer ?? defaultKeyStoreInitializer,
firebaseEmulatorHost: userConfig.firebaseEmulatorHost,
redirects: userConfig.redirects,
} satisfies VerifySessionCookieFirebaseAuthConfig
// TODO(codehex): will be supported
const checkRevoked = false
return async (c, next) => {
const auth = Auth.getOrInitialize(
config.projectId,
config.keyStore ?? config.keyStoreInitializer(c)
)
const session = getCookie(c, config.cookieName)
if (session === undefined) {
const res = c.redirect(config.redirects.signIn)
throw new HTTPException(res.status, { res, message: 'session is empty' })
}
try {
const idToken = await auth.verifySessionCookie(session, checkRevoked, {
FIREBASE_AUTH_EMULATOR_HOST:
config.firebaseEmulatorHost ?? c.env.FIREBASE_AUTH_EMULATOR_HOST,
})
setFirebaseToken(c, idToken)
} catch (err) {
const res = c.redirect(config.redirects.signIn)
throw new HTTPException(res.status, {
res,
message: `failed to verify the requested firebase token: ${String(err)}`,
})
}
await next()
}
}