
# AI Gateway Authentication Policy

:::note{title="AI Gateway Policy"}

This policy is for use with the [AI Gateway](/docs/ai-gateway/overview). See
the AI Gateway documentation to learn how to configure and govern AI models
with Zuplo.

:::

The AI Gateway Authentication policy protects an AI Gateway with application API
keys. It identifies the calling application on `request.user` (`sub` is the
application name, `data` its metadata) and puts the application's AI Gateway
configuration into effect for the policies later in the request pipeline.

Add it to an application's `inboundPolicyChain` to require a key for that app
only, or place it on the route before the configuration executor to require a
key for every application on the route. Clients can send keys with the standard
`Authorization: Bearer` header or with a custom header and scheme.
Authentication results can be cached briefly to reduce request latency.

## Configuration

The configuration shows how to configure the policy in the 'policies.json' document.

```json title="config/policies.json"
{
  "name": "my-ai-gateway-auth-inbound-policy",
  "policyType": "ai-gateway-auth-inbound",
  "handler": {
    "export": "AIGatewayAuthInboundPolicy",
    "module": "$import(@zuplo/runtime)",
    "options": {
      "cacheTtlSeconds": 10,
      "credentialPassthrough": false
    }
  }
}
```

### Policy Configuration

- `name` <code className="text-green-600">&lt;string&gt;</code> - The name of your policy instance. This is used as a reference in your routes.
- `policyType` <code className="text-green-600">&lt;string&gt;</code> - The identifier of the policy. This is used by the Zuplo UI. Value should be `ai-gateway-auth-inbound`.
- `handler.export` <code className="text-green-600">&lt;string&gt;</code> - The name of the exported type. Value should be `AIGatewayAuthInboundPolicy`.
- `handler.module` <code className="text-green-600">&lt;string&gt;</code> - The module containing the policy. Value should be `$import(@zuplo/runtime)`.
- `handler.options` <code className="text-green-600">&lt;object&gt;</code> - The options for this policy. [See Policy Options](#policy-options) below.

### Policy Options

The options for this policy are specified below. All properties are optional unless specifically marked as required.

- `cacheTtlSeconds` <code className="text-green-600">&lt;number&gt;</code> - The time to cache authentication results for a particular key. Higher values will decrease latency. Cached results will be valid until the cache expires even in the event the key is deleted, etc. Defaults to `10`.
- `authHeader` <code className="text-green-600">&lt;string&gt;</code> - The request header that carries the Zuplo application key. When credentialPassthrough is enabled, use a header other than Authorization, such as zp-gateway-api-key. Defaults to `"Authorization"`.
- `authScheme` <code className="text-green-600">&lt;string&gt;</code> - The scheme that prefixes the key in the authHeader header, for example Bearer. Set to an empty string when the header contains only the key. Defaults to `"Bearer"`.
- `credentialPassthrough` <code className="text-green-600">&lt;boolean&gt;</code> - Forward the caller's Authorization header to the primary provider as its credential. Zuplo reads the application key from authHeader, which must be a header other than Authorization. Defaults to `false`.

## Using the Policy

The AI Gateway Authentication policy authenticates requests with an AI Gateway
application API key. After a key is accepted, the calling application is
identified on `request.user` — `sub` is the application name and `data` its
metadata, both readable from custom policies — and the application's AI Gateway
configuration (its selected policy chain, model access, and limits) takes effect
for the request through the AI Gateway policies later in the route.

Use this policy when an application (or every application on a route) must
present an AI Gateway application API key.

A missing or invalid key returns `401 Unauthorized`. A valid key that belongs to
a different application than the route's `app_id` returns `403 Forbidden`.

## Prerequisites

Create the AI Gateway applications and their API keys before calling the
gateway. Each client must use a key issued for one of those applications.

## Add the policy

Declare the policy in `config/policies.json`:

```json
{
  "policies": [
    {
      "name": "ai-gateway-auth-inbound",
      "policyType": "ai-gateway-auth",
      "handler": {
        "export": "AIGatewayAuthInboundPolicy",
        "module": "$import(@zuplo/runtime)",
        "options": {
          "cacheTtlSeconds": 60
        }
      }
    }
  ]
}
```

Authentication is optional. Choose where to attach it based on how widely keys
should be required:

### Per application (app-level)

Add the policy to an application's `inboundPolicyChain`. Only that application
requires an API key; other applications on the same route can omit it. The route
should run the configuration executor alone so it can load configuration from
the `app_id` path parameter before the app chain runs:

```json
{
  "inboundPolicyChain": [
    {
      "name": "ai-gateway-auth-inbound"
    }
  ]
}
```

### Every application on the route (route-level)

Add the policy on the route **before** the configuration loader (or before the
executor on executor-only routes). That requires an API key for every
application that hits the route:

```json
{
  "x-zuplo-route": {
    "corsPolicy": "none",
    "handler": {
      "export": "aiGatewayHandlerV2",
      "module": "$import(@zuplo/runtime)",
      "options": {}
    },
    "policies": {
      "inbound": [
        "ai-gateway-auth-inbound",
        "ai-gateway-configuration-loader-inbound",
        "ai-gateway-configuration-executor-inbound"
      ]
    }
  }
}
```

When auth runs on the route, it loads the app configuration from the key and the
loader/executor reuse that channel. When auth is omitted from the route (or only
present in some apps' chains), the loader (or the executor alone) loads
configuration from the route's `app_id` path parameter instead.

> **Protect every public gateway.** Without this policy, the gateway resolves
> the application from the URL and accepts the request without an application
> key. Only leave authentication out when another control restricts access:
> network isolation, or the
> [Ensure Gateway Internal Invocation Only](/docs/policies/ai-gateway-internal-only-inbound)
> policy for an application that is only called from inside the gateway with
> `context.invokeRoute`. Team policy templates can include a locked auth entry
> so new applications require keys by default.

## Call the gateway

By default, clients send the application key as a bearer token:

```bash
curl https://gateway.example.com/v1/chat/completions \
  --header "Authorization: Bearer YOUR_APP_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "model": "openai/gpt-5-mini",
    "messages": [{ "role": "user", "content": "Hello" }]
  }'
```

Authentication scheme matching is case-insensitive. A missing key, an invalid
scheme, or a key that is not authorized receives a `401 Unauthorized` response.

Once the key is accepted, the policy removes the `authHeader` header from the
request. The application key never reaches an upstream provider or a policy
later in the chain, whatever header name it arrived in. Read the caller's
identity from `request.user` instead.

## Use a custom header

Set `authHeader` to accept the key from another header. Set `authScheme` to an
empty string when the header contains only the key:

```json
{
  "name": "ai-gateway-auth-inbound",
  "policyType": "ai-gateway-auth",
  "handler": {
    "export": "AIGatewayAuthInboundPolicy",
    "module": "$import(@zuplo/runtime)",
    "options": {
      "authHeader": "x-api-key",
      "authScheme": "",
      "cacheTtlSeconds": 60
    }
  }
}
```

Clients can then call the gateway with:

```bash
curl https://gateway.example.com/v1/chat/completions \
  --header "x-api-key: YOUR_APP_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "model": "openai/gpt-5-mini",
    "messages": [{ "role": "user", "content": "Hello" }]
  }'
```

## Pass through a caller's provider credential

Set `credentialPassthrough` to `true` and configure a separate header for the
Zuplo application key. For a bare key in `zp-gateway-api-key`, use these policy
options:

```json
{
  "credentialPassthrough": true,
  "authHeader": "zp-gateway-api-key",
  "authScheme": ""
}
```

Send the provider credential in `Authorization`. `authHeader` and `authScheme`
remain authoritative; sending a special header does not enable passthrough or
override your configuration. The defaults remain `Authorization` and `Bearer`,
so enabling passthrough without configuring a separate header is a configuration
error.

For example:

```bash
curl https://gateway.example.com/v1/chat/completions \
  --header "zp-gateway-api-key: YOUR_ZUPLO_APP_KEY" \
  --header "Authorization: Bearer YOUR_PROVIDER_CREDENTIAL" \
  --header "Content-Type: application/json" \
  --data '{
    "model": "provider/model",
    "messages": [{ "role": "user", "content": "Hello" }]
  }'
```

After the Zuplo application key succeeds, the policy moves the provider
credential into private request-scoped state and removes both authentication
headers from the mutable inbound request. The primary provider receives the
original `Authorization` value exactly as supplied, instead of its configured
credential. Retries of the primary use that same caller credential. Error and
quota fallbacks use their own configured credentials, never the caller's token.
Fallbacks therefore require configured credentials and can incur charges on the
gateway owner's provider account. On native create requests at `/v1/messages`
and `/v1/responses`, a compatible, explicitly configured backup runs only after
the primary exhausts upstream 429 retries. It uses the backup's configured
credential while preserving the native response and stream format; these
endpoints do not use timeout, transport-error, or 5xx fallback. Semantic-cache
policies skip passthrough requests so a cache hit cannot bypass upstream
authentication. ZuploDemo is excluded because it must continue to use the
gateway's deployment credential.

When the primary provider is OpenAI and the passed-through credential is a
ChatGPT login token, such as the one Codex CLI sends after `codex login`, the
gateway sends `/v1/responses` requests to the ChatGPT Codex backend at
`https://chatgpt.com/backend-api/codex`. OpenAI only accepts those tokens there;
the public API rejects them. Requests that pass through an OpenAI platform API
key keep going to `https://api.openai.com`. The ChatGPT backend only speaks
streaming. For a request with `stream: false`, the gateway streams from the
backend and returns the completed response as one JSON body, the same shape
`https://api.openai.com` returns for a non-streaming request.

A missing, empty, or invalid dedicated key fails authentication; the gateway
never retries with `Authorization` as the Zuplo key. A missing or empty
`Authorization` also returns 401 in this mode.

With `credentialPassthrough` omitted or `false`, existing authentication
applies: `authHeader` and `authScheme` select the Zuplo key, and providers use
their configured credentials. Moving the gateway key to an arbitrary custom
header alone does not enable passthrough.

## Choose a cache duration

`cacheTtlSeconds` controls how long an authentication result can be reused. The
minimum value is 10 seconds.

- Use a shorter duration when key changes must take effect quickly.
- Use a longer duration to reduce authentication latency and repeated validation
  work.

A revoked key can continue to work until its cached result expires.

## Options

- `cacheTtlSeconds`: Number of seconds to cache an authentication result.
  Defaults to `10` and must be at least `10`.
- `authHeader`: Header containing the application key. Defaults to
  `Authorization`. With `credentialPassthrough`, use a different header, such as
  `zp-gateway-api-key`.
- `authScheme`: Scheme before the key. Defaults to `Bearer`. Use `""` when the
  header contains only the key.
- `credentialPassthrough`: Forward the caller's `Authorization` header to the
  primary provider as its credential while Zuplo reads the application key from
  `authHeader`. Defaults to `false`.

Read more about [how policies work](/articles/policies)
