Authorization and integrity

When building any public-facing application, it's extremely important to protect the data stored in your system. When it comes to LLMs, extra diligence is necessary to ensure that the model is only accessing data it should, tool calls are properly scoped to the user invoking the LLM, and the flow is being invoked only by verified client applications.

Firebase Genkit provides mechanisms for managing authorization policies and contexts. For flows running on Cloud Functions for Firebase, developers are required to provide an auth policy or else explicitly acknowledge the lack of one. For non-Functions flows, auth can be managed and set as well, but requires a bit more manual integration.

Basic flow authorization

All flows can define an authPolicy in their config. An auth policy is a function that tests if certain criteria (defined by you) are met, and throws an exception if any test fails. If this field is set, it is executed before the flow is invoked:

import { defineFlow, runFlow } from '@genkit-ai/flow';

export const selfSummaryFlow = defineFlow(
  {
    name: 'selfSummaryFlow',
    inputSchema: z.object({uid: z.string()}),
    outputSchema: z.string(),
    authPolicy: (auth, input) => {
      if (!auth) {
        throw new Error('Authorization required.');
      }
      if (input.uid !== auth.uid) {
        throw new Error('You may only summarize your own profile data.');
      }
    }
  },
  async (input) => { ... });

When executing this flow, you must provide an auth object using withLocalAuthContext or else you'll receive an error:

// Error: Authorization required.
await runFlow(selfSummaryFlow, { uid: 'abc-def' });

// Error: You may only summarize your own profile data.
await runFlow(
  selfSummaryFlow,
  { uid: 'abc-def' },
  {
    withLocalAuthContext: { uid: 'hij-klm' },
  }
);

// Success
await runFlow(
  selfSummaryFlow,
  { uid: 'abc-def' },
  {
    withLocalAuthContext: { uid: 'abc-def' },
  }
);

When running with the Genkit Development UI, you can pass the Auth object by entering JSON in the "Auth JSON" tab: {"uid": "abc-def"}.

You can also retrieve the auth context for the flow at any time within the flow by calling getFlowAuth(), including in functions invoked by the flow:

import { getFlowAuth, defineFlow } from '@genkit-ai/flow';

async function readDatabase(uid: string) {
  if (getFlowAuth().admin) {
    // Do something special if the user is an admin:
    ...
  } else {
    // Otherwise, use the `uid` variable to retrieve the relevant document
    ...
  }
}

export const selfSummaryFlow = defineFlow(
  {
    name: 'selfSummaryFlow',
    inputSchema: z.object({uid: z.string()}),
    outputSchema: z.string(),
    authPolicy: ...
  },
  async (input) => {
    ...
    await readDatabase(input.uid);
  });

When testing flows with Genkit dev tools, you are able to specify this auth object in the UI, or on the command line with the --auth flag:

genkit flow:run selfSummaryFlow '{"uid": "abc-def"}' --auth '{"uid": "abc-def"}'

Cloud Functions for Firebase integration

The Firebase plugin provides convenient integration with Firebase Auth / Google Cloud Identity Platform as well as built-in Firebase App Check support.

Authorization

The onFlow() wrapper provided by the Firebase plugin works natively with the Cloud Functions for Firebase client SDKs. When using the SDK, the Firebase Auth header will automatically be included as long as your app client is also using the Firebase Auth SDK. You can use Firebase Auth to protect your flows defined with onFlow():

import {firebaseAuth} from "@genkit-ai/firebase/auth";
import {onFlow} from "@genkit-ai/firebase/functions";

export const selfSummaryFlow = onFlow({
    name: "selfSummaryFlow",
    inputSchema: z.string(),
    outputSchema: z.string(),
    authPolicy: firebaseAuth((user) => {
      if (!user.email_verified && !user.admin) {
        throw new Error("Email not verified");
      }
    }),
  }, (subject) => {...})

When using the Firebase Auth plugin, user will be returned as a DecodedIdToken. You can always retrieve this object at any time via getFlowAuth() as noted above. When running this flow during development, you would pass the user object in the same way:

genkit flow:run selfSummaryFlow '{"uid": "abc-def"}' --auth '{"admin": true}'

By default the Firebase Auth plugin requires the auth header to be sent by the client, but in cases where you wish to allow unauthenticated access with special handling for authenticated users (upselling features, say), then you can configure the policy like so:

authPolicy: firebaseAuth((user) => {
  if (user && !user.email_verified) {
    throw new Error("Logged in users must have verified emails");
  }
}, {required: false}),

Whenever you expose a Cloud Function to the wider internet, it is vitally important that you use some sort of authorization mechanism to protect your data and the data of your customers. With that said, there are times when you need to deploy a Cloud Function with no code-based authorization checks (for example, your Function is not world-callable but instead is protected by Cloud IAM). The authPolicy field is always required when using onFlow(), but you can indicate to the library that you are forgoing authorization checks by using the noAuth() function:

import {onFlow, noAuth} from "@genkit-ai/firebase/functions";

export const selfSummaryFlow = onFlow({
    name: "selfSummaryFlow",
    inputSchema: z.string(),
    outputSchema: z.string(),
    // WARNING: Only do this if you have some other gatekeeping in place, like
    // Cloud IAM!
    authPolicy: noAuth(),
  }, (subject) => {...})

Client integrity

Authentication on its own goes a long way to protect your app. But it's also important to ensure that only your client apps are calling your functions. The Firebase plugin for genkit includes first-class support for Firebase App Check. Simply add the following configuration options to your onFlow():

import {onFlow} from "@genkit-ai/firebase/functions";

export const selfSummaryFlow = onFlow({
    name: "selfSummaryFlow",
    inputSchema: z.string(),
    outputSchema: z.string(),

    // These two fields for app check. The consumeAppCheckToken option is for
    // replay protection, and requires additional client configuration. See the
    // App Check docs.
    enforceAppCheck: true,
    consumeAppCheckToken: true,

    authPolicy: ...,
  }, (subject) => {...})

Non-Firebase HTTP authorization

When deploying flows to a server context outside of Cloud Functions for Firebase, you'll want to have a way to set up your own authorization checks alongside the native flows. You have two options:

  1. Use whatever server framework you like, and pass the auth context through via runFlow() as noted above.

  2. Use the built-in startFlowsServer() and provide Express middleware in the flow config:

    export const selfSummaryFlow = defineFlow(
    {
      name: 'selfSummaryFlow',
      inputSchema: z.object({uid: z.string()}),
      outputSchema: z.string(),
      middleware: [
        (req, res, next) => {
          const token = req.headers['authorization'];
          const user = yourVerificationLibrary(token);
    
          // This is what will get passed to your authPolicy
          req.auth = user;
          next();
        }
      ],
      authPolicy: (auth, input) => {
        if (!auth) {
          throw new Error('Authorization required.');
        }
        if (input.uid !== auth.uid) {
          throw new Error('You may only summarize your own profile data.');
        }
      }
    },
    async (input) => { ... });
    
    startFlowsServer();  // This will register the middleware
    

    For more information about using Express, see the Cloud Run instructions.

Please note, if you go with (1), you the middleware configuration option will be ignored by runFlow().