157 lines
5.1 KiB
TypeScript
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()
|
|
}
|
|
}
|