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

157 lines
5.1 KiB
TypeScript

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