{"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 { DecodedAppCheckToken, getAppCheck } from 'firebase-admin/app-check';\nimport { DecodedIdToken, getAuth } from 'firebase-admin/auth';\nimport { UserFacingError } from 'genkit';\nimport { ContextProvider, RequestData } from 'genkit/context';\nimport { initializeAppIfNecessary } from './helpers.js';\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';\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 function (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":[]}