UNPKG

@genkit-ai/firebase

Version:

Genkit AI framework plugin for Firebase including Firestore trace/state store and deployment helpers for Cloud Functions for Firebase.

1 lines 16.2 kB
{"version":3,"sources":["../src/context.ts"],"sourcesContent":["/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n getAppCheck,\n type DecodedAppCheckToken,\n} from 'firebase-admin/app-check';\nimport { getAuth, type DecodedIdToken } from 'firebase-admin/auth';\n// @ts-ignore - `firebase` is an optional peer dep, don't error if it's missing\nimport type {\n FirebaseApp,\n FirebaseOptions,\n FirebaseServerApp,\n} from 'firebase/app';\nimport { UserFacingError } from 'genkit';\nimport type { ContextProvider, RequestData } from 'genkit/context';\nimport { initializeAppIfNecessary } from './helpers.js';\n\n/**\n * Debug features that can be enabled to simplify testing.\n * These features are in a JSON object for FIREBASE_DEBUG_FEATURES and only take\n * effect if FIREBASE_DEBUG_MODE=true.\n *\n * Do not set these variables in production.\n */\nexport interface DebugFeatures {\n skipTokenVerification?: boolean;\n}\n\nlet cachedDebugSkipTokenVerification: boolean | undefined;\n\nexport function setDebugSkipTokenVerification(skip: boolean) {\n cachedDebugSkipTokenVerification = skip;\n}\n\nfunction debugSkipTokenVerification(): boolean {\n if (cachedDebugSkipTokenVerification !== undefined) {\n return cachedDebugSkipTokenVerification;\n }\n if (!process.env.FIREBASE_DEBUG_MODE) {\n return false;\n }\n if (!process.env.FIREBASE_DEBUG_FEATURES) {\n return false;\n }\n const features = JSON.parse(\n process.env.FIREBASE_DEBUG_FEATURES\n ) as DebugFeatures;\n cachedDebugSkipTokenVerification = features.skipTokenVerification ?? false;\n return cachedDebugSkipTokenVerification;\n}\n\n/**\n * The type of data that will be added to an Action's context when using the fireabse middleware.\n * You can safely cast Action's context to a Firebase Context to help type checking and code complete.\n */\nexport interface FirebaseContext {\n /**\n * Information about the authorized user.\n * This comes from the Authentication header, which is a JWT bearer token.\n * Will be omitted if auth is not defined or the key is invalid. To reject requests in these cases\n * set signedIn in a declarative policy or check in a policy callback.\n */\n auth?: {\n uid: string;\n token: DecodedIdToken;\n rawToken: string;\n };\n\n /**\n * Information about the AppCheck token for a request.\n * This comes form the X-Firebase-AppCheck header and is included in the firebase-functions\n * client libraries (which can be used for Genkit requests irrespective of whether they're hosted\n * on Firebase).\n * Will be omitted if AppCheck tokens are invalid. To reject requests in these cases,\n * set enforceAppCheck in a declaritve policy or check in a policy callback.\n */\n app?: {\n appId: string;\n token: DecodedAppCheckToken;\n alreadyConsumed?: boolean;\n rawToken: string;\n };\n\n /**\n * An unverified token for a Firebase Instance ID.\n */\n instanceIdToken?: string;\n\n /**\n * A FirebaseServerApp with the same Auth and App Check credentials as the request.\n */\n firebaseApp?: FirebaseServerApp;\n}\n\nexport interface FirebaseContextProvider<I = any>\n extends ContextProvider<FirebaseContext, I> {\n (request: RequestData<I>): Promise<FirebaseContext>;\n}\n\n/**\n * Helper methods that provide most common needs for an authorization policy.\n */\nexport interface DeclarativePolicy {\n /**\n * Requires the user to be signed in or not.\n * Implicitly part of hasClaims.\n */\n signedIn?: boolean;\n\n /**\n * Requires the user's email to be verified.\n * Requires the user to be signed in.\n */\n emailVerified?: boolean;\n\n /**\n * Clam or Claims that must be present in the request.\n * Can be a singel claim name or array of claim names to merely test the presence\n * of a clam or can be an object of claim names and values that must be present.\n * Requires the user to be signed in.\n */\n hasClaim?: string | string[] | Record<string, string>;\n\n /**\n * Whether appCheck must be enforced\n */\n enforceAppCheck?: boolean;\n\n /**\n * Whether app check enforcement includes consuming tokens.\n * Consuming tokens adds more security at the cost of performance.\n */\n consumeAppCheckToken?: boolean;\n\n /**\n * Either a FirebaseApp or the options used to initialize one. When provided,\n * `context.firebaseApp` will be populated as a FirebaseServerApp with the current\n * request's auth and app check credentials allowing you to perform actions using\n * Firebase Client SDKs authenticated as the requesting user.\n *\n * You must have the `firebase` dependency in your `package.json` to use this option.\n */\n serverAppConfig?: FirebaseApp | FirebaseOptions;\n}\n\n/**\n * Calling firebaseContext() without any parameters merely parses firebase context data.\n * It does not do any validation on the data found. To do automatic validation,\n * pass either an options object or function for freeform validation.\n */\nexport function firebaseContext<I = any>(): FirebaseContextProvider<I>;\n\n/**\n * Calling firebaseContext() with a declarative policy both parses and enforces context.\n * Honors the same environment variables that Cloud Functions for Firebase does to\n * mock token validation in preproduction environmets.\n */\nexport function firebaseContext<I = any>(\n policy: DeclarativePolicy\n): FirebaseContextProvider<I>;\n\n/**\n * Calling firebaseContext() with a policy callback parses context but delegates enforcement.\n * To control the message sent to a user, throw UserFacingError.\n * For security reasons, other error types will be returned as a 500 \"internal error\".\n */\nexport function firebaseContext<I = any>(\n policy: (context: FirebaseContext, input: I) => void | Promise<void>\n): FirebaseContextProvider<I>;\n\nexport function firebaseContext<I = any>(\n policy?:\n | DeclarativePolicy\n | ((context: FirebaseContext, input: I) => void | Promise<void>)\n): FirebaseContextProvider<I> {\n return async (request: RequestData): Promise<FirebaseContext> => {\n initializeAppIfNecessary();\n let auth: FirebaseContext['auth'];\n\n const authIdToken = extractBearerToken(request.headers['authorization']);\n const appCheckToken = request.headers['x-firebase-appcheck'];\n\n if ('authorization' in request.headers) {\n auth = await verifyAuthToken(authIdToken);\n }\n let app: FirebaseContext['app'];\n if ('x-firebase-appcheck' in request.headers) {\n const consumeAppCheckToken =\n typeof policy === 'object' && policy['consumeAppCheckToken'];\n app = await verifyAppCheckToken(\n appCheckToken,\n consumeAppCheckToken ?? false\n );\n }\n let instanceIdToken: FirebaseContext['instanceIdToken'];\n if ('firebase-instance-id-token' in request.headers) {\n instanceIdToken = request.headers['firebase-instance-id-token'];\n }\n const context: FirebaseContext = {};\n\n if (typeof policy === 'object' && policy.serverAppConfig) {\n // we dynamically import here to keep `firebase` an optional peer dep\n const { initializeServerApp } = await import('firebase/app');\n context.firebaseApp = initializeServerApp(policy.serverAppConfig, {\n appCheckToken,\n authIdToken,\n releaseOnDeref: context,\n });\n }\n\n if (auth) {\n context.auth = auth;\n }\n if (app) {\n context.app = app;\n }\n if (instanceIdToken) {\n context.instanceIdToken = instanceIdToken;\n }\n if (typeof policy === 'function') {\n await policy(context, request.input);\n } else if (typeof policy === 'object') {\n enforceDelcarativePolicy(policy, context);\n }\n return context;\n };\n}\n\nfunction verifyHasClaims(claims: string[], token: DecodedIdToken) {\n for (const claim of claims) {\n if (!token[claim] || token[claim] === 'false') {\n if (claim == 'email_verified') {\n throw new UserFacingError(\n 'PERMISSION_DENIED',\n 'Email must be verified'\n );\n }\n if (claim === 'admin') {\n throw new UserFacingError('PERMISSION_DENIED', 'Must be an admin');\n }\n throw new UserFacingError(\n 'PERMISSION_DENIED',\n `${claim} claim is required`\n );\n }\n }\n}\n\nfunction enforceDelcarativePolicy(\n policy: DeclarativePolicy,\n context: FirebaseContext\n) {\n if (\n (policy.signedIn || policy.hasClaim || policy.emailVerified) &&\n !context.auth\n ) {\n throw new UserFacingError('UNAUTHENTICATED', 'Auth is required');\n }\n if (policy.hasClaim) {\n if (typeof policy.hasClaim === 'string') {\n verifyHasClaims([policy.hasClaim], context.auth!.token);\n } else if (Array.isArray(policy.hasClaim)) {\n verifyHasClaims(policy.hasClaim, context.auth!.token);\n } else if (typeof policy.hasClaim === 'object') {\n for (const [claim, value] of Object.entries(policy.hasClaim)) {\n if (context.auth!.token[claim] !== value) {\n throw new UserFacingError(\n 'PERMISSION_DENIED',\n `Claim ${claim} must be ${value}`\n );\n }\n }\n } else {\n // Not a user facing error so this turns into a log + 500 internal to the user.\n throw Error(`Invalid type ${typeof policy.hasClaim} for hasClaim`);\n }\n }\n if (policy.emailVerified) {\n verifyHasClaims(['email_verified'], context.auth!.token);\n }\n if (policy.enforceAppCheck && !context.app) {\n throw new UserFacingError(\n 'PERMISSION_DENIED',\n `AppCheck token is required`\n );\n }\n}\n\nfunction extractBearerToken(authHeader: string): string | undefined {\n return /[bB]earer (.*)/.exec(authHeader)?.[1];\n}\n\nasync function verifyAuthToken(\n token?: string\n): Promise<FirebaseContext['auth']> {\n if (!token) {\n return undefined;\n }\n if (debugSkipTokenVerification()) {\n const decoded = unsafeDecodeToken(token) as DecodedIdToken;\n return {\n uid: decoded['sub'],\n token: decoded,\n rawToken: token,\n };\n }\n try {\n const decoded = await getAuth().verifyIdToken(token);\n return {\n uid: decoded['sub'],\n token: decoded,\n rawToken: token,\n };\n } catch (err) {\n console.error(`Error decoding auth token: ${err}`);\n throw new UserFacingError('PERMISSION_DENIED', 'Invalid auth token');\n }\n}\n\nasync function verifyAppCheckToken(\n token: string,\n consumeAppCheckToken: boolean\n): Promise<FirebaseContext['app']> {\n if (debugSkipTokenVerification()) {\n const decoded = unsafeDecodeToken(token) as DecodedAppCheckToken;\n return {\n appId: decoded['sub'],\n token: decoded,\n alreadyConsumed: false,\n rawToken: token,\n };\n }\n try {\n return {\n ...(await getAppCheck().verifyToken(token, {\n consume: consumeAppCheckToken,\n })),\n rawToken: token,\n };\n } catch (err) {\n console.error(`Got error verifying AppCheck token: ${err}`);\n throw new UserFacingError('PERMISSION_DENIED', 'Invalid AppCheck token');\n }\n}\n\nexport function fakeToken(claims: Record<string, string>): string {\n return `fake.${Buffer.from(JSON.stringify(claims), 'utf-8').toString('base64')}.fake`;\n}\n\nconst TOKEN_REGEX = /[a-zA-Z0-9_=-]+\\.[a-zA-Z0-9_=-]+\\.[a-zA-Z0-9_=-]+/;\nfunction unsafeDecodeToken(token: string): Record<string, unknown> {\n if (!TOKEN_REGEX.test(token)) {\n throw new UserFacingError(\n 'PERMISSION_DENIED',\n 'Invalid fake token. Use the fakeToken() method to create a valid fake token'\n );\n }\n try {\n return JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());\n } catch (err) {\n throw new UserFacingError(\n 'PERMISSION_DENIED',\n 'Invalid fake token. Use the fakeToken() method to create a valid fake token'\n );\n }\n}\n"],"mappings":"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","names":[]}