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. Flows running on Firebase can use an auth policy callback (or helper). Alternatively, Firebase also provides auth context into the flow where it can do its own checks. For non-Functions flows, auth can be managed and set through middleware.
Authorize within a Flow
Flows can check authorization in two ways: either the request binding
(e.g. onCallGenkit
for Cloud Functions for Firebase or express
) can enforce
authorization, or those frameworks can pass auth policies to the flow itself,
where the flow has access to the information for auth managed within the
flow.
import { genkit, z, UserFacingError } from 'genkit';
const ai = genkit({ ... });
export const selfSummaryFlow = ai.defineFlow( {
name: 'selfSummaryFlow',
inputSchema: z.object({ uid: z.string() }),
outputSchema: z.string(),
}, async (input, { context }) => {
if (!context.auth) {
throw new UserFacingErrorError('UNAUTHENTICATED', 'Unauthenticated');
}
if (input.uid !== context.auth.uid) {
throw new UserFacingError('PERMISSION_DENIED', 'You may only summarize your own profile data.');
}
// Flow logic here...
});
It is up to the request binding to populate context.auth
in this case. For
example, onCallGenkit
automatically populates context.auth
(Firebase Authentication), context.app
(Firebase App Check), and
context.instanceIdToken
(Firebase Cloud Messaging). When calling a flow
manually, you can add your own auth context manually.
// Error: Authorization required.
await selfSummaryFlow({ uid: 'abc-def' });
// Error: You may only summarize your own profile data.
await selfSummaryFlow.run(
{ uid: 'abc-def' },
{
context: { auth: { uid: 'hij-klm' } },
}
);
// Success
await selfSummaryFlow(
{ uid: 'abc-def' },
{
context: { auth: { 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 ai.currentContext()
, including in functions invoked by the flow:
import { genkit, z } from 'genkit';
const ai = genkit({ ... });;
async function readDatabase(uid: string) {
const auth = ai.currentContext()?.auth;
// Note: the shape of context.auth depends on the provider. onCallGenkit puts
// claims information in auth.token
if (auth?.token?.admin) {
// Do something special if the user is an admin
} else {
// Otherwise, use the `uid` variable to retrieve the relevant document
}
}
export const selfSummaryFlow = ai.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 --context
flag:
genkit flow:run selfSummaryFlow '{"uid": "abc-def"}' --context '{"auth": {"email_verified": true}}'
Authorize using Cloud Functions for Firebase
The Cloud Functions for Firebase SDKs support Genkit including integration with Firebase Auth / Google Cloud Identity Platform, as well as built-in Firebase App Check support.
User authentication
The onCallGenkit()
wrapper provided by the Firebase Functions library has
built-in support for the Cloud Functions for Firebase
client SDKs.
When you use these SDKs, the Firebase Auth header is automatically 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 onCallGenkit()
:
import { genkit } from 'genkit';
import { onCallGenkit } from 'firebase-functions/https';
const ai = genkit({ ... });;
const selfSummaryFlow = ai.defineFlow({
name: 'selfSummaryFlow',
inputSchema: z.string(),
outputSchema: z.string(),
}, async (input) => {
// Flow logic here...
});
export const selfSummary = onCallGenkit({
authPolicy: (auth) => auth?.token?.['email_verified'] && auth?.token?.['admin'],
}, selfSummaryFlow);
When you use onCallGenkit
, context.auth
is returned as an object with
a uid
for the user ID, and a token
that is a
DecodedIdToken.
You can always retrieve this object at any time using ai.currentContext()
as
noted earlier. When running this flow during development, you would pass the
user object in the same way:
genkit flow:run selfSummaryFlow '{"uid": "abc-def"}' --context '{"auth": {"admin": true}}'
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).
Cloud Functions for Firebase lets you to do this using the invoker
property,
which controls IAM access. The special value 'private'
leaves the function as
the default IAM setting, which means that only callers with the
Cloud Run Invoker role
can execute the function. You can instead provide the email address of a user
or service account that should be granted permission to call this exact
function.
import { onCallGenkit } from 'firebase-functions/https'
const selfSummaryFlow = ai.defineFlow({
name: 'selfSummaryFlow',
inputSchema: z.string(),
outputSchema: z.string(),
}, async (input) => {
// Flow logic here...
});
export const selfSummary = onCallGenkit({
invoker: 'private',
}, selfSummaryFlow);
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. Do this by
adding the following configuration options to your onCallGenkit()
:
import { onCallGenkit } from 'firebase-functions/https';
const selfSummaryFlow = ai.defineFlow({
name: 'selfSummaryFlow',
inputSchema: z.string(),
outputSchema: z.string(),
}, async (input) => {
// Flow logic here...
});
export const selfSummary = onCallGenkit({
// 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: ...,
}, selfSummaryFlow);
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 built-in flows.
Use a ContextProvider
to populate context values such as auth
, and to
provide a declarative policy or a policy callback. The Genkit
SDK provides ContextProvider
s such as apiKey
, and plugins may
expose them as well. For example, the @genkit-ai/firebase/context
plugin
exposes a context provider for verifying Firebase Auth credentials and
populating them into context.
With code like the following, which might appear in a variety of applications:
// Express app with a simple API key
import { genkit, z } from 'genkit';
const ai = genkit({ ... });;
export const selfSummaryFlow = ai.defineFlow(
{
name: 'selfSummaryFlow',
inputSchema: z.object({ uid: z.string() }),
outputSchema: z.string(),
},
async (input) => {
// Flow logic here...
}
);
You could secure a simple "flow server" express app by writing:
import { apiKey } from "genkit";
import { startFlowServer, withContext } from "@genkit-ai/express";
startFlowServer({
flows: [
withContext(selfSummaryFlow, apiKey(process.env.REQUIRED_API_KEY))
],
});
Or you could build a custom express application using the same tools:
import { apiKey } from "genkit";
import * as express from "express";
import { expressHandler } from "@genkit-ai/express;
const app = express();
// Capture but don't validate the API key (or its absence)
app.post('/summary', expressHandler(selfSummaryFlow, { contextProvider: apiKey()}))
app.listen(process.env.PORT, () => {
console.log(`Listening on port ${process.env.PORT}`);
})
ContextProvider
s abstract out the web framework, so these
tools work in other frameworks like Next.js as well. Here is an example of a
Firebase app built on Next.js.
import { appRoute } from "@genkit-ai/express";
import { firebaseContext } from "@genkit-ai/firebase";
export const POST = appRoute(selfSummaryFlow, { contextProvider: firebaseContext })
For more information about using Express, see the Cloud Run instructions.