Authorization and integrity
When you put a flow behind a public HTTP endpoint, three things have to hold: the caller is who they claim to be, every tool call is scoped to that caller, and the model cannot talk its way into a wider scope. Genkit for Go gives you one mechanism for all three — action context — plus the error classification that turns a rejection into the right HTTP status.
Authenticate at the handler
Section titled “Authenticate at the handler”Attach a context provider to the handler. It runs before the flow, so a rejection never reaches your business logic:
import ( "context" "net/http" "strings"
"github.com/firebase/genkit/go/core" "github.com/firebase/genkit/go/core/status" "github.com/firebase/genkit/go/genkit")
func authProvider(verify func(context.Context, string) (*Claims, error)) core.ContextProvider { return func(ctx context.Context, req core.RequestData) (core.ActionContext, error) { token := strings.TrimPrefix(req.Headers["authorization"], "Bearer ") if token == "" { return nil, status.Errorf(status.ErrUnauthenticated, "missing bearer token") } claims, err := verify(ctx, token) if err != nil { // The message stays server-side; the client gets a bare 401. return nil, status.Errorf(status.ErrUnauthenticated, "token rejected: %w", err) } return core.ActionContext{ "uid": claims.Subject, "tenant": claims.Tenant, "scopes": claims.Scopes, }, nil }}
mux.Handle("POST /summarize", genkit.Handler(summarizeFlow, genkit.WithContextProviders(authProvider(verifyIDToken))))Header names in req.Headers are lower-cased, so match on "authorization".
Map a rejection to the right status
Section titled “Map a rejection to the right status”The HTTP status comes from the error’s classification, not from the handler:
| Return | Client sees |
|---|---|
status.Errorf(status.ErrUnauthenticated, ...) | 401 |
status.Errorf(status.ErrPermissionDenied, ...) | 403 |
status.Errorf(status.ErrInvalidArgument, ...) | 400 |
errors.New(...) or any unclassified error | 500 |
Messages are withheld from the client unless you build them with
status.PublicErrorf, which is the right default: an auth failure message is
exactly the kind of text that leaks internal structure. The full error is
always logged server-side.
Authorize inside the flow
Section titled “Authorize inside the flow”Authentication says who the caller is. Authorization says what this particular request may touch, and that usually depends on the input:
summarizeFlow := genkit.DefineFlow(g, "summarize", func(ctx context.Context, in SummarizeInput) (string, error) { actx := core.FromContext(ctx) uid, _ := actx["uid"].(string) if uid == "" { // Reachable when the flow is called in-process without context. return "", status.Errorf(status.ErrUnauthenticated, "no caller identity") } doc, err := store.Get(ctx, in.DocID) if err != nil { return "", err } if doc.OwnerID != uid { return "", status.Errorf(status.ErrPermissionDenied, "user %s does not own document %s", uid, in.DocID) } // ... })Keep the check inside the flow, not only in the provider. The same flow runs from a worker, a test, and the Developer UI, and only the HTTP path goes through a context provider.
Scope tool calls to the caller
Section titled “Scope tool calls to the caller”This is the part that is specific to LLM applications. A tool that takes the user ID as an input field lets the model choose whose data to read, and a prompt injection in retrieved content or user text can make it choose someone else’s. Take the identity from the action context instead:
// Wrong: the model supplies customerID, so the model decides whose orders to read.type badInput struct { CustomerID string `json:"customerId"`}
// Right: the tool takes only what the model legitimately chooses. Identity comes// from the request, which the model cannot influence.type ordersInput struct { Status string `json:"status" jsonschema:"enum=open,enum=shipped"`}
listOrders := genkit.DefineTool(g, "listOrders", "Lists the signed-in customer's orders.", func(toolCtx *ai.ToolContext, in ordersInput) ([]Order, error) { uid, _ := core.FromContext(toolCtx.Context)["uid"].(string) if uid == "" { return nil, status.Errorf(status.ErrUnauthenticated, "no signed-in user") } return store.OrdersFor(toolCtx.Context, uid, in.Status) })Apply the same rule to retrievers: filter by tenant inside the retriever, never by a filter value the model produced.
Verify the client, not just the user
Section titled “Verify the client, not just the user”Authenticating the end user does not stop a scraper replaying your endpoint with a stolen token, and some deployments have no end user at all. For those, add a check on the calling application: a service-to-service identity token, a signed request from your own frontend, or App Check-style attestation. It goes in the same context provider, as a second gate before the user check.
Deployment notes
Section titled “Deployment notes”- Cloud Run. Either let the platform authenticate (deploy without
--allow-unauthenticatedand require an ID token, which keeps unauthenticated traffic off your container entirely), or accept public traffic and configure application-level authentication as described above. See Deploy with Cloud Run. - The Developer UI reflection server is not authenticated. It only starts
when
GENKIT_ENV=dev. Ensure it is not enabled in production deployments. - Agent HTTP routes need the same treatment; see Serve agents over HTTP.
Learn more
Section titled “Learn more”- Passing information through context — the mechanism these examples are built on
- Error types — the full status set and what reaches a client
- Testing your AI logic — asserting that an unauthenticated request really is rejected