---
title: "OIDC - JWT-based Auth"
url: "/docs/proxy/token_auth"
canonical_url: "https://docs.litellm.ai/docs/proxy/token_auth"
type: "docs"
last_updated: "2026-10-06"
summary: "Use JWT's to auth admins / users / projects into the proxy."
related:
  - "/docs/proxy/virtual_keys"
  - "/docs/proxy/jwt_key_mapping"
---
# OIDC - JWT-based Auth

> Index of all LiteLLM docs: https://docs.litellm.ai/llms.txt


Use JWT's to auth admins / users / projects into the proxy.

> **LiteLLM Enterprise feature: JWT-based Auth.** Requires an Enterprise license (`LITELLM_LICENSE`). Talk to sales: https://www.litellm.ai/enterprise#talk-to-sales

:::tip[JWT → Virtual Key Mapping]

Want per-user model restrictions, spend limits, and rate limits without distributing API keys? See **[JWT → Virtual Key Mapping](./jwt_key_mapping.md)** for granular access control for JWT-authenticated users (e.g. Claude Code + SSO).

:::

## Usage

### Step 1. Setup Proxy

- `JWT_PUBLIC_KEY_URL`: This is the public keys endpoint of your OpenID provider. Typically it's `{openid-provider-base-url}/.well-known/openid-configuration/jwks`. For Keycloak it's `{keycloak_base_url}/realms/{your-realm}/protocol/openid-connect/certs`.
- `JWT_AUDIENCE`: This is the audience used for decoding the JWT. If not set, the decode step will not verify the audience. 

```bash
export JWT_PUBLIC_KEY_URL="" # "https://demo.duendesoftware.com/.well-known/openid-configuration/jwks"
```

- `enable_jwt_auth` in your config. This will tell the proxy to check if a token is a jwt token.

```yaml
general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
  enable_jwt_auth: True

model_list:
- model_name: azure-gpt-3.5 
  litellm_params:
      model: azure/<your-deployment-name>
      api_base: os.environ/AZURE_API_BASE
      api_key: os.environ/AZURE_API_KEY
      api_version: "2023-07-01-preview"
```

### Step 2. Create JWT with scopes 

**admin**

Create a client scope called `litellm_proxy_admin` in your OpenID provider (e.g. Keycloak).

Grant your user, `litellm_proxy_admin` scope when generating a JWT. 

```bash
curl --location ' 'https://demo.duendesoftware.com/connect/token'' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id={CLIENT_ID}' \
--data-urlencode 'client_secret={CLIENT_SECRET}' \
--data-urlencode 'username=test-{USERNAME}' \
--data-urlencode 'password={USER_PASSWORD}' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'scope=litellm_proxy_admin' # 👈 grant this scope
```
**project**

Create a JWT for your project on your OpenID provider (e.g. Keycloak).

```bash
# client_id: 👈 project id
curl --location ' 'https://demo.duendesoftware.com/connect/token'' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id={CLIENT_ID}' \
--data-urlencode 'client_secret={CLIENT_SECRET}' \
--data-urlencode 'grant_type=client_credential' \
```

### Step 3. Test your JWT 

**/key/generate**

```bash
curl --location '{proxy_base_url}/key/generate' \
--header 'Authorization: Bearer eyJhbGciOiJSUzI1NiI...' \
--header 'Content-Type: application/json' \
--data '{}'
```
**/chat/completions**

```bash
curl --location 'http://0.0.0.0:4000/v1/chat/completions' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJhbGciOiJSUzI1...' \
--data '{"model": "azure-gpt-3.5", "messages": [ { "role": "user", "content": "What's the weather like in Boston today?" } ]}'
```

## Advanced

### Multiple OIDC providers

Use this if you want LiteLLM to validate your JWT against multiple OIDC providers (e.g. Google Cloud, GitHub Auth)

Set `JWT_PUBLIC_KEY_URL` in your environment to a comma-separated list of URLs for your OIDC providers. Each entry can be a JWKS URL or an OIDC discovery URL (`.../.well-known/openid-configuration`); LiteLLM fetches a discovery document and follows its `jwks_uri`

```bash
export JWT_PUBLIC_KEY_URL="https://demo.duendesoftware.com/.well-known/openid-configuration,https://accounts.google.com/.well-known/openid-configuration"
```

This validates tokens from every listed provider against one shared set of claim mappings. If your providers disagree about what a given claim contains, use per-issuer claim mapping instead

#### How a token is validated

LiteLLM reads the `kid` from the unverified JWT header and walks the `JWT_PUBLIC_KEY_URL` list in order. For each URL it loads that provider's key set and looks for a key whose `kid` equals the token's. The first match wins and the search stops there; if no listed provider publishes that `kid`, the request is rejected with a 401 (`No matching public key found`). The `iss` claim plays no part in choosing the key set on this path, so `kid` alone decides which provider's key is tried

A token without a `kid` header only matches a key set that contains exactly one key. Against a JWKS with several keys it is rejected, so a provider that rotates keys must put `kid` in the header

Once a key is selected the signature is verified with it and `exp` (plus `nbf` and `iat` when present) is checked. The accepted algorithms are RS256/384/512, PS256/384/512, ES256/384/512 and EdDSA; HMAC-signed tokens (`HS*`) are always rejected. Key material is read from the JWK `kty`, `n`, `e`, `x`, `y` and `crv` members, so RSA, EC and OKP keys all work and `x5c` certificate chains are ignored

`aud` and `iss` are only verified when you ask for it. `JWT_AUDIENCE` sets the expected audience (a token whose `aud` is a list passes if it contains that value) and `JWT_ISSUER` sets the expected `iss`. Both are single values shared by every URL in the list, so with several providers `JWT_ISSUER` can only admit one of them; a setup that needs `iss` verified for more than one provider must use [per-issuer configuration](#per-issuer-claim-mapping). Leaving both unset means any token signed by any listed provider is accepted no matter which application it was minted for, and the proxy logs a warning at first use to say so

#### Caching and failure behavior

Each key set (and each discovery document) is cached in the proxy's auth cache, Redis when configured and otherwise in-process memory, for `litellm_jwtauth.public_key_ttl` seconds (default 600). A `kid` miss does not trigger a refetch inside the TTL, so a token signed with a key the provider rotated in a moment ago is rejected until the cached copy expires

When a fetch fails at the transport level (DNS, connect, TLS, timeout) LiteLLM tries it up to three times with a short backoff, then remembers the outage for 30 seconds so concurrent requests do not pile onto the dead endpoint. If a last-known-good copy of that key set exists and is younger than `public_key_ttl + public_key_stale_ttl` (default 600 + 3600 seconds) it is served and a warning is logged. Otherwise the request fails with a 503 (`the identity provider's JWKS endpoint is temporarily unreachable`) and the remaining URLs in the list are not tried, even if one of them holds the matching key. Set `public_key_stale_ttl: 0` to fail closed as soon as the cached copy expires. A non-200 response or an unparseable body is neither retried nor served stale; it fails the request with a 401 and also stops the walk through the list

```yaml title="config.yaml"
general_settings:
  enable_jwt_auth: true
  litellm_jwtauth:
    public_key_ttl: 600
    public_key_stale_ttl: 3600
```

#### Per-issuer claim mapping

Use `litellm_jwtauth.issuers` when the same claim means different things depending on which provider minted the token. A common case: one provider puts the team ID in `sub` while another puts the user ID there, so a single global `team_id_jwt_field: "sub"` cannot serve both

Each entry is matched against the token's `iss` claim and carries its own JWKS URL, audience, and claim mappings

```yaml title="config.yaml"
general_settings:
  enable_jwt_auth: true
  litellm_jwtauth:
    admin_jwt_scope: "litellm_proxy_admin"
    issuers:
      - issuer: "https://accounts.google.com"
        audience: "my-gcp-audience"
        team_id_jwt_field: "sub"

      - issuer: "https://keycloak.example.com/realms/my-realm"
        jwks_url: "https://keycloak.example.com/realms/my-realm/protocol/openid-connect/certs"
        audience: "my-keycloak-audience"
        user_id_jwt_field: "sub"
        user_email_jwt_field: "email"
```

With this config a token from `accounts.google.com` resolves `sub` to a team ID, while a token from Keycloak resolves the same `sub` to a user ID

##### Per-issuer fields

| Field | Required | Description |
| --- | --- | --- |
| `issuer` | Yes | Exact expected `iss` claim value. Matching is an exact string comparison |
| `audience` | Yes, unless `disable_audience_validation` is set | Expected `aud` for tokens from this issuer |
| `disable_audience_validation` | Yes, unless `audience` is set | Skip audience validation for this issuer. Setting both this and `audience` is rejected |
| `jwks_url` | No | This issuer's JWKS endpoint. Defaults to reading `<issuer>/.well-known/openid-configuration` and following its `jwks_uri` |
| `team_id_jwt_field` | No | Claim path to read as the team ID |
| `team_ids_jwt_field` | No | Claim path to read as a list of team IDs |
| `user_id_jwt_field` | No | Claim path to read as the user ID |
| `user_email_jwt_field` | No | Claim path to read as the user email |
| `org_id_jwt_field` | No | Claim path to read as the organization ID |
| `end_user_id_jwt_field` | No | Claim path to read as the end-user ID |

Claim paths support dot notation for nested claims, for example `resource_access.my-client.team`

##### Things to know

Every issuer must set either `audience` or `disable_audience_validation: true`. LiteLLM rejects the config at startup otherwise, so tokens minted by another application that shares your provider's signing keys cannot authenticate against your proxy

Claim mappings you leave out of an issuer entry fall back to the top-level `litellm_jwtauth` setting. If you keep a global `team_id_jwt_field: "sub"` and add an issuer that only maps `user_id_jwt_field: "sub"`, that issuer's tokens still get a team ID read from `sub`. Move all of your claim mappings into `issuers` to avoid this

`issuers` is additive routing rather than an allow-list. A token whose `iss` matches no entry falls through to the global `JWT_PUBLIC_KEY_URL` and `JWT_AUDIENCE` path, so leave those unset if you want only your listed issuers accepted

Matching is on `iss` only. The `kid` header still selects the signing key within the matched issuer's JWKS

#### Recommended multi-provider setup

For more than one provider, prefer `litellm_jwtauth.issuers` over the shared list even when the claim mappings are the same. An `issuers` entry scopes the key lookup to that issuer's own JWKS and verifies `iss` and `aud` per provider, which the shared list cannot do. Leave `JWT_PUBLIC_KEY_URL` unset so a token whose `iss` is not listed is rejected (401, `Missing JWT Public Key URL`) instead of falling through to an unscoped path. Key caching and the stale-copy fallback described above apply to each issuer's JWKS in the same way

```yaml title="config.yaml"
general_settings:
  enable_jwt_auth: true
  litellm_jwtauth:
    user_id_jwt_field: "sub"
    user_email_jwt_field: "email"
    issuers:
      - issuer: "https://keycloak.example.com/realms/my-realm"
        audience: "litellm-proxy"

      - issuer: "https://sts.example-cloud.com"
        jwks_url: "https://sts.example-cloud.com/.well-known/jwks.json"
        audience: "litellm-proxy"
```

#### Provider compatibility

LiteLLM does not special-case any identity provider. Any issuer works if it signs tokens with one of the algorithms listed above, publishes its public keys as a JWKS (directly, or through an OIDC discovery document that carries a `jwks_uri`), sets a `kid` header that appears in that JWKS, and mints a stable `iss` value plus the audience you configure. The Keycloak and Kubernetes issuers shown on this page publish keys this way; for any other issuer, check those four points against a decoded sample token and the provider's JWKS before relying on it

#### Troubleshooting

Run the proxy with `--detailed_debug` to see the `JWT Auth:` log lines that explain each rejection

| Symptom | Cause | Fix |
| --- | --- | --- |
| 401 `No matching public key found. keys=[...], kid=...` | No listed JWKS contains the token's `kid`, or the token has no `kid` and the JWKS has several keys | Confirm the `kid` from the token header appears in one of the JWKS documents. If the provider just rotated keys, wait out `public_key_ttl` or restart the proxy |
| 401 `Validation fails: Signature verification failed` with several providers in `JWT_PUBLIC_KEY_URL` | Two providers publish the same `kid`; the shared list picks the first URL that has it and verifies against the wrong key | Use `litellm_jwtauth.issuers` so the lookup is scoped to the token's issuer |
| 401 `Validation fails: Invalid issuer` | `iss` does not equal `JWT_ISSUER` (shared path) or the matched `issuers[].issuer` | Copy `iss` verbatim from a decoded token; trailing slashes and `http` vs `https` matter |
| 401 `Validation fails: Audience doesn't match` or `Token is missing the "aud" claim` | `aud` does not contain `JWT_AUDIENCE` / `issuers[].audience`, or the token has no `aud` | Set the audience your provider actually mints (often the client ID), or set `disable_audience_validation: true` on that issuer if it cannot mint one |
| 401 `Validation fails: The specified alg value is not allowed` | Token is HMAC-signed or uses an algorithm outside the list above | Configure the provider to sign with RS256 or another asymmetric algorithm |
| 401 `OIDC discovery document at ... does not contain a 'jwks_uri' field` | The URL contains `.well-known/openid-configuration`, so it was treated as a discovery document, but it returned a JWKS | Point `JWT_PUBLIC_KEY_URL` at the discovery document itself, or at a JWKS URL whose path does not contain that segment |
| 503 `the identity provider's JWKS endpoint is temporarily unreachable` | A JWKS or discovery URL could not be reached and no stale copy was available | Check egress from the proxy to the provider. Raise `public_key_stale_ttl` to ride out longer outages |
| 401 `Missing JWT Public Key URL from environment` | `iss` matched no `issuers` entry and `JWT_PUBLIC_KEY_URL` is unset | Add the issuer to `issuers`, or set `JWT_PUBLIC_KEY_URL` if unlisted issuers should be accepted |

### Kubernetes ServiceAccount Authentication

Use Kubernetes ServiceAccount tokens to authenticate workloads running in your cluster. This is useful when you want pods to authenticate to LiteLLM using their native Kubernetes identity.

#### Prerequisites

1. Your Kubernetes cluster must have ServiceAccount token projection enabled (default in Kubernetes 1.20+)
2. Your cluster's OIDC issuer must be accessible (for EKS, GKE, AKS this is automatic)

#### Step 1: Configure the OIDC Discovery URL

Set `JWT_PUBLIC_KEY_URL` to your cluster's OIDC discovery endpoint:

**Amazon EKS**

```bash
# Get your EKS OIDC issuer URL
aws eks describe-cluster --name <cluster-name> --query "cluster.identity.oidc.issuer" --output text

# Set the JWKS URL (append /keys to the issuer URL)
export JWT_PUBLIC_KEY_URL="https://oidc.eks.<region>.amazonaws.com/id/<id>/keys"
```

**Google GKE**

```bash
# GKE uses Google's OIDC provider
export JWT_PUBLIC_KEY_URL="https://container.googleapis.com/v1/projects/<project>/locations/<location>/clusters/<cluster>/jwks"
```

**Azure AKS**

```bash
# Get your AKS OIDC issuer URL
az aks show --name <cluster-name> --resource-group <resource-group> --query "oidcIssuerProfile.issuerUrl" -o tsv

# Set the JWKS URL
export JWT_PUBLIC_KEY_URL="<issuer-url>/openid/v1/jwks"
```

**Self-Managed**

```bash
# For self-managed clusters, check your API server's --service-account-issuer flag
# The JWKS endpoint is typically at:
export JWT_PUBLIC_KEY_URL="https://<api-server>/openid/v1/jwks"
```

#### Step 2: Configure LiteLLM

Configure LiteLLM to extract identity information from Kubernetes ServiceAccount tokens:

```yaml
general_settings:
  enable_jwt_auth: True
  litellm_jwtauth:  
    # Use namespace as team identifier (resolves via team_alias in DB)
    team_alias_jwt_field: 'kubernetes\.io.namespace'
```

#### Step 3: Create ServiceAccount and Configure Pod

Create a ServiceAccount with an associated secret and configure your pod to use the token:

```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-llm-client
  namespace: my-app
---
apiVersion: v1
kind: Secret
metadata:
  name: my-llm-client-token
  namespace: my-app
  annotations:
    kubernetes.io/service-account.name: my-llm-client
type: kubernetes.io/service-account-token
---
apiVersion: v1
kind: Pod
metadata:
  name: llm-client-pod
  namespace: my-app
spec:
  serviceAccountName: my-llm-client
  containers:
  - name: app
    image: my-app:latest
    env:
    - name: LITELLM_TOKEN
      valueFrom:
        secretKeyRef:
          name: my-llm-client-token
          key: token
```

Set the expected audience in LiteLLM:

```bash
export JWT_AUDIENCE="https://kubernetes.default.svc"
```

#### Step 4: Create Team for Namespace

Create a team in LiteLLM that matches the namespace (using `team_alias`):

```bash
curl -X POST 'http://0.0.0.0:4000/team/new' \
-H 'Authorization: Bearer <PROXY_MASTER_KEY>' \
-H 'Content-Type: application/json' \
-d '{
    "team_alias": "my-app",
    "team_id": "my-app",
    "models": ["gpt-5.6-terra", "claude-sonnet-5"]
}'
```

#### Step 5: Use the Token

From within the pod, the token is available in the `LITELLM_TOKEN` environment variable:

```bash
# Make a request to LiteLLM using the env var
curl -X POST 'http://0.0.0.0:4000/v1/chat/completions' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $LITELLM_TOKEN" \
-d '{
  "model": "gpt-5.6-terra",
  "messages": [{"role": "user", "content": "Hello!"}]
}'
```

#### Example: ServiceAccount Token Structure

A Kubernetes ServiceAccount token looks like this:

```json
{
  "aud": ["litellm-proxy"],
  "exp": 1234567890,
  "iat": 1234567890,
  "iss": "https://oidc.eks.us-west-2.amazonaws.com/id/EXAMPLE",
  "kubernetes.io": {
    "namespace": "my-app",
    "pod": {
      "name": "llm-client-pod",
      "uid": "pod-uid"
    },
    "serviceaccount": {
      "name": "my-llm-client",
      "uid": "sa-uid"
    }
  },
  "nbf": 1234567890,
  "sub": "system:serviceaccount:my-app:my-llm-client"
}
```

#### Advanced: Map Namespace to Team Using Name Resolution

Use the `team_alias_jwt_field` to automatically resolve namespaces to teams:

```yaml
general_settings:
  enable_jwt_auth: True
  litellm_jwtauth:
    user_id_jwt_field: "sub"
    # Map the namespace to team_alias in the database
    team_alias_jwt_field: 'kubernetes\.io.namespace'
    user_id_upsert: true
```

This way, pods in namespace `production` automatically get associated with the team that has `team_alias: production`.

### Set Accepted JWT Scope Names 

Change the string in JWT 'scopes', that litellm evaluates to see if a user has admin access.

```yaml
general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
  enable_jwt_auth: True
  litellm_jwtauth:
    admin_jwt_scope: "litellm-proxy-admin"
```

### Tracking End-Users / Internal Users / Team / Org

Set the field in the jwt token, which corresponds to a litellm user / team / org.

**Note:** All JWT fields support dot notation to access nested claims (e.g., `"user.sub"`, `"resource_access.client.roles"`).

```yaml
general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
  enable_jwt_auth: True
  litellm_jwtauth:
    admin_jwt_scope: "litellm-proxy-admin"
    team_id_jwt_field: "client_id" # 👈 CAN BE ANY FIELD (supports dot notation for nested claims)
    user_id_jwt_field: "sub" # 👈 CAN BE ANY FIELD (supports dot notation for nested claims)
    org_id_jwt_field: "org_id" # 👈 CAN BE ANY FIELD (supports dot notation for nested claims)
    end_user_id_jwt_field: "customer_id" # 👈 CAN BE ANY FIELD (supports dot notation for nested claims)
```

Expected JWT (flat structure): 

```json
{
  "client_id": "my-unique-team",
  "sub": "my-unique-user",
  "org_id": "my-unique-org"
}
```

**Or with nested structure using dot notation:**

```json
{
  "user": {
    "sub": "my-unique-user",
    "email": "user@example.com"
  },
  "tenant": {
    "team_id": "my-unique-team"
  },
  "organization": {
    "id": "my-unique-org"
  }
}
```

**Configuration for nested example:**

```yaml
litellm_jwtauth:
  user_id_jwt_field: "user.sub"
  user_email_jwt_field: "user.email"
  team_id_jwt_field: "tenant.team_id"
  org_id_jwt_field: "organization.id"
```

Now litellm will automatically update the spend for the user/team/org in the db for each call. 

### Resolve by Name (Alias) Instead of ID

Sometimes your JWT token contains human-readable names instead of database IDs. LiteLLM can resolve these names to IDs by looking them up in the database.

**Use Case:** Your IDP provides team/org names in the JWT, but LiteLLM needs the actual database IDs for spend tracking and access control.

```yaml
general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
  enable_jwt_auth: True
  litellm_jwtauth:
    # Name-based fields (resolved via database lookup)
    team_alias_jwt_field: "team_alias"       # Resolves team by team_alias in DB
    org_alias_jwt_field: "org_alias"         # Resolves org by organization_alias in DB
```

**Expected JWT:**

```json
{
  "sub": "user-123",
  "team_alias": "engineering-team",
  "org_alias": "acme-corp"
}
```

**How It Works:**

1. LiteLLM extracts the name from the configured JWT field
2. Looks up the entity in the database by its alias field:
   - Teams: `team_alias` column in `LiteLLM_TeamTable`
   - Organizations: `organization_alias` column in `LiteLLM_OrganizationTable`
3. Uses the resolved ID for spend tracking and access control

**Precedence:** ID fields always take precedence over name fields. If both `team_id_jwt_field` and `team_alias_jwt_field` are configured and both values exist in the JWT, the ID will be used.

```yaml
# Example: ID takes precedence
litellm_jwtauth:
  team_id_jwt_field: "team_id"        # Used if present in JWT
  team_alias_jwt_field: "team_alias"   # Fallback if team_id not present
```

**Nested Fields:** Name fields also support dot notation for nested claims:

```yaml
litellm_jwtauth:
  team_alias_jwt_field: "organization.team.name"
  org_alias_jwt_field: "company.name"
```

**Important Notes:**
- The entity (team/org) must already exist in the database with the matching alias
- Aliases should be unique - if multiple entities share the same alias, an error will be returned
- Name resolution adds a database lookup, so using IDs directly is slightly more performant

### JWT Scopes

Here's what scopes on JWT-Auth tokens look like

**Can be a list**
```
scope: ["litellm-proxy-admin",...]
```

**Can be a space-separated string**
```
scope: "litellm-proxy-admin ..."
```

### Control model access with Teams

1. Specify the JWT field that contains the team ids, that the user belongs to. 

```yaml
general_settings:
  enable_jwt_auth: True
  litellm_jwtauth:
    user_id_jwt_field: "sub"
    team_ids_jwt_field: "groups" 
    user_id_upsert: true # add user_id to the db if they don't exist
    enforce_team_based_model_access: true # don't allow users to access models unless the team has access
```

This is assuming your token looks like this:
```
{
  ...,
  "sub": "my-unique-user",
  "groups": ["team_id_1", "team_id_2"]
}
```

2. Create the teams on LiteLLM 

```bash
curl -X POST '<PROXY_BASE_URL>/team/new' \
-H 'Authorization: Bearer <PROXY_MASTER_KEY>' \
-H 'Content-Type: application/json' \
-D '{
    "team_alias": "team_1",
    "team_id": "team_id_1" # 👈 MUST BE THE SAME AS THE SSO GROUP ID
}'
```

3. Test the flow

SSO for UI: [**See Walkthrough**](https://www.loom.com/share/8959be458edf41fd85937452c29a33f3?sid=7ebd6d37-569a-4023-866e-e0cde67cb23e)

OIDC Auth for API: [**See Walkthrough**](https://www.loom.com/share/00fe2deab59a426183a46b1e2b522200?sid=4ed6d497-ead6-47f9-80c0-ca1c4b6b4814)

### Flow

- Validate if user id is in the DB (LiteLLM_UserTable)
- Validate if any of the groups are in the DB (LiteLLM_TeamTable)
- Validate if any group has model access
- If all checks pass, allow the request

### Select Team via Request Header

When a JWT token contains multiple teams (via `team_ids_jwt_field`), you can explicitly select which team to use for a request by passing the `x-litellm-team-id` header.

The header accepts either the team's `team_id` or its `team_alias`. LiteLLM first checks the value against the team ids the JWT grants; when it is not one of them, LiteLLM looks up a team with that alias and accepts it only if that team's id is one the JWT grants. Either way the request runs as the canonical `team_id`, so budgets, model access, rate limits, spend logs and the `team_id` column in the database all show the id, never the alias. Sending the id skips the alias lookup

```bash
curl -X POST 'http://0.0.0.0:4000/v1/chat/completions' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <your-jwt-token>' \
-H 'x-litellm-team-id: team_id_2' \
-d '{
  "model": "gpt-5.6-terra",
  "messages": [{"role": "user", "content": "Hello"}]
}'
```

The same request with the alias of `team_id_2` (the `team_alias` set on `/team/new`) resolves to the same team:

```bash
curl -X POST 'http://0.0.0.0:4000/v1/chat/completions' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <your-jwt-token>' \
-H 'x-litellm-team-id: team_2' \
-d '{
  "model": "gpt-5.6-terra",
  "messages": [{"role": "user", "content": "Hello"}]
}'
```

**Validation:**
- The value must be a team id in the JWT's `team_ids_jwt_field` list (or the `team_id_jwt_field` value), or the alias of one of those teams
- A value that is neither, including the alias of a team the JWT does not grant, returns a 403 whose message says the value matched no team id or team alias and lists the team ids the JWT allows
- An alias shared by more than one team never resolves; keep aliases unique if you want to select teams by alias
- If no header is provided, LiteLLM auto-selects the first team with access to the requested model

With `fallback_to_db_teams: true` and a JWT that carries no team claim, the header is checked against the user's team memberships in the database instead of the JWT, and an alias is accepted there too: the value must be the id or the alias of a team the user belongs to, otherwise the request is denied with a 403

### Fall back to DB team when JWT claims don't resolve

By default, when `team_id_jwt_field` or `team_ids_jwt_field` is configured and the JWT carries a claim value that does **not** map to any LiteLLM team, LiteLLM raises an error: the claim is treated as authoritative.

For deployments where the IdP team claim is **advisory** (e.g. machine tokens whose `groups` claim lives in a separate namespace from LiteLLM `team_id`s), opt in to a fallback: if the configured claim is present but unresolved, LiteLLM defers to the user's single LiteLLM team (when the user belongs to exactly one team in the DB).

```yaml
general_settings:
  enable_jwt_auth: True
  litellm_jwtauth:
    user_id_jwt_field: "sub"
    team_ids_jwt_field: "groups"
    team_claim_fallback: true # 👈 opt in
```

**Behavior:**

| Trigger | Default (`team_claim_fallback: false`) | Opt-in (`team_claim_fallback: true`) |
|---|---|---|
| `team_id` claim resolves to a real team | 200 / use team | 200 / use team |
| `team_id` claim present, team missing in DB | raise | defer → fallback to user's single DB team |
| `team_alias` claim resolves | 200 / use team | 200 / use team |
| `groups` claim resolves and team grants model | 200 | 200 |
| `groups` claim resolves but team lacks model | 403 (preserved) | 403 (preserved) |
| `groups` claim present, none resolve to a real team | 403 | defer → fallback to user's single DB team |
| no claim at all (single-team fallback baseline) | 200 / fallback | 200 / fallback |

**Security envelope:** the fallback only resolves when the user belongs to exactly one LiteLLM team in the DB; non-404 errors (e.g. `"No DB Connected"`) always propagate. Keep the default (`false`) if your IdP team claims are authoritative for authorization.

### Custom JWT Validate

Validate a JWT Token using custom logic, if you need an extra way to verify if tokens are valid for LiteLLM Proxy.

#### 1. Setup custom validate function

```python
from typing import Literal

def my_custom_validate(token: str) -> Literal[True]:
  """
  Only allow tokens with tenant-id == "my-unique-tenant", and claims == ["proxy-admin"]
  """
  allowed_tenants = ["my-unique-tenant"]
  allowed_claims = ["proxy-admin"]

  if token["tenant_id"] not in allowed_tenants:
    raise Exception("Invalid JWT token")
  if token["claims"] not in allowed_claims:
    raise Exception("Invalid JWT token")
  return True
```

#### 2. Setup config.yaml

```yaml
general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
  enable_jwt_auth: True
  litellm_jwtauth:
    user_id_jwt_field: "sub"
    team_id_jwt_field: "tenant_id"
    user_id_upsert: True
    custom_validate: custom_validate.my_custom_validate # 👈 custom validate function
```

#### 3. Test the flow

**Expected JWT**

```
{
  "sub": "my-unique-user",
  "tenant_id": "INVALID_TENANT",
  "claims": ["proxy-admin"]
}
```

**Expected Response**

```
{
  "error": "Invalid JWT token"
}
```

### Allowed Routes 

Configure which routes a JWT can access via the config.

By default: 

- Admins: can access only management routes (`/team/*`, `/key/*`, `/user/*`)
- Teams: can access only openai routes (`/chat/completions`, etc.)+ info routes (`/*/info`)

[**See Code**](https://github.com/BerriAI/litellm/blob/b204f0c01c703317d812a1553363ab0cb989d5b6/litellm/proxy/_types.py#L95)

**Admin Routes**
```yaml
general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
  enable_jwt_auth: True
  litellm_jwtauth:
    admin_jwt_scope: "litellm-proxy-admin"
    admin_allowed_routes: ["/v1/embeddings"]
```

**Team Routes**
```yaml
general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
  enable_jwt_auth: True
  litellm_jwtauth:
    # ...
    team_id_jwt_field: "litellm-team" # 👈 Set field in the JWT token that stores the team ID
    team_allowed_routes: ["/v1/chat/completions"] # 👈 Set accepted routes
```

### Allowing other provider routes for Teams

Team JWTs can already call `/v1/messages` and `/v1/messages/count_tokens` by default. To enable team JWT tokens to access other Anthropic-style endpoints in `anthropic_routes`, update `team_allowed_routes` in your `litellm_jwtauth` configuration. `team_allowed_routes` supports the following values:

- Named route groups from `LiteLLMRoutes` (e.g., `openai_routes`, `anthropic_routes`, `info_routes`, `mapped_pass_through_routes`).
- Exact routes, e.g. `/v1/messages`.
- Route prefixes ending in `*`, e.g. `/internal-models/*`, which match every route under that prefix.

Below is a quick reference for the route groups you can use and example representative routes from each group. If you need the exhaustive list, see the `LiteLLMRoutes` enum in `litellm/proxy/_types.py` for the authoritative list.

| Route Group | What it contains | Representative routes |
|-------------|------------------|-----------------------|
| `openai_routes` | OpenAI-compatible REST endpoints (chat, completion, embeddings, images, responses, models, etc.) | `/v1/chat/completions`, `/v1/completions`, `/v1/embeddings`, `/v1/images/generations`, `/v1/models` |
| `anthropic_routes` | Anthropic-style endpoints (`/v1/messages` and related) | `/v1/messages`, `/v1/messages/count_tokens`, `/v1/skills` |
| `mapped_pass_through_routes` | Provider-specific pass-through route prefixes (e.g., Anthropic when proxied via `/anthropic`). Use with `mapped_pass_through_routes` for provider wildcard mapping | `/anthropic/*`, `/vertex-ai/*`, `/bedrock/*` |
| `passthrough_routes_wildcard` | Wildcard mapping for providers (e.g., `/anthropic/*`) - precomputed wildcard list used by the proxy | `/anthropic/*`, `/vllm/*` |
| `google_routes` | Google-specific (e.g., Vertex / Batching endpoints) | `/v1beta/models/{model_name}:generateContent` |
| `mcp_routes` | Internal MCP management endpoints | `/mcp/tools`, `/mcp/tools/call` |
| `info_routes` | Read-only & info endpoints used by the UI | `/key/info`, `/team/info`, `/v1/models` |
| `management_routes` | Admin-only management endpoints (create/update/delete user/team/model) | `/team/new`, `/key/generate`, `/model/new` |
| `spend_tracking_routes` | Budget/spend related endpoints | `/spend/logs`, `/spend/keys`, `/spend/users` |
| `public_routes` | Public and unauthenticated endpoints | `/`, `/routes`, `/.well-known/litellm-ui-config` |

Note: `llm_api_routes` is the union of OpenAI, Anthropic, Google, pass-through and other LLM routes (`openai_routes + anthropic_routes + google_routes + mapped_pass_through_routes + passthrough_routes_wildcard + apply_guardrail_routes + mcp_routes + litellm_native_routes`).

Defaults (what the proxy uses if you don't override them in `litellm_jwtauth`):

- `admin_jwt_scope`: `litellm_proxy_admin`
- `admin_allowed_routes` (default): `management_routes`, `spend_tracking_routes`, `global_spend_tracking_routes`, `info_routes` 
- `team_allowed_routes` (default): `openai_routes`, `info_routes`, `mcp_routes`, `/v1/messages`, `/v1/messages/count_tokens`. Setting `team_allowed_routes` replaces this list, so add `mcp_routes` and the `/v1/messages` routes back if teams still need them
- `public_allowed_routes` (default): `public_routes`

Example: Allow team JWTs to call Anthropic `/v1/messages` (either by route group or by explicit route string):

```yaml
general_settings:
  enable_jwt_auth: True
  litellm_jwtauth:
    team_ids_jwt_field: "team_ids"
    team_allowed_routes: ["openai_routes", "info_routes", "anthropic_routes"]
```

Or selectively allow the exact Anthropic message endpoint only:

```yaml
general_settings:
  enable_jwt_auth: True
  litellm_jwtauth:
    team_ids_jwt_field: "team_ids"
    team_allowed_routes: ["/v1/messages", "info_routes"]
```

If you register pass-through endpoints that all share a prefix, grant the prefix once with a trailing `*` so routes added later are covered without another config change. `admin_allowed_routes` accepts the same patterns.

```yaml
general_settings:
  enable_jwt_auth: True
  litellm_jwtauth:
    team_ids_jwt_field: "team_ids"
    team_allowed_routes: ["openai_routes", "info_routes", "/internal-models/*"]
```

Only a trailing `*` is treated as a wildcard, and it matches routes below the prefix, so `/internal-models/*` covers `/internal-models/anthropic/v1/messages` but not the bare `/internal-models` route.

### Caching Public Keys 

Control how long public keys are cached for (in seconds).

```yaml
general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
  enable_jwt_auth: True
  litellm_jwtauth:
    admin_jwt_scope: "litellm-proxy-admin"
    admin_allowed_routes: ["/v1/embeddings"]
    public_key_ttl: 600 # 👈 KEY CHANGE
```

### Custom JWT Field 

Set a custom field in which the team_id exists. By default, the 'client_id' field is checked. 

```yaml
general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
  enable_jwt_auth: True
  litellm_jwtauth:
    team_id_jwt_field: "client_id" # 👈 KEY CHANGE
```

### Block Teams 

To block all requests for a certain team id, use `/team/block`

**Block Team**

```bash
curl --location 'http://0.0.0.0:4000/team/block' \
--header 'Authorization: Bearer <admin-token>' \
--header 'Content-Type: application/json' \
--data '{
    "team_id": "litellm-test-client-id-new" # 👈 set team id
}'
```

**Unblock Team**

```bash
curl --location 'http://0.0.0.0:4000/team/unblock' \
--header 'Authorization: Bearer <admin-token>' \
--header 'Content-Type: application/json' \
--data '{
    "team_id": "litellm-test-client-id-new" # 👈 set team id
}'
```

### Upsert Users + Allowed Email Domains 

Allow users who belong to a specific email domain, automatic access to the proxy.

**Note:** `user_allowed_email_domain` is optional. If not specified, all users will be allowed regardless of their email domain.
 
```yaml
general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
  enable_jwt_auth: True
  litellm_jwtauth:
    user_email_jwt_field: "email" # 👈 checks 'email' field in jwt payload
    user_allowed_email_domain: "my-co.com" # 👈 OPTIONAL - allows user@my-co.com to call proxy
    user_id_upsert: true # 👈 upserts the user to db, if valid email but not in db
```

## OIDC UserInfo Endpoint

Use this when your JWT/access token doesn't contain user-identifying information. LiteLLM will call your identity provider's UserInfo endpoint to fetch user details.

### When to Use

- Your JWT is opaque (not self-contained) or lacks user claims
- You need to fetch fresh user information from your identity provider
- Your access tokens don't include email, roles, or other identifying data

### Configuration

```yaml title="config.yaml" showLineNumbers
general_settings:
  enable_jwt_auth: True
  litellm_jwtauth:
    # Enable OIDC UserInfo endpoint
    oidc_userinfo_enabled: true
    oidc_userinfo_endpoint: "https://your-idp.com/oauth2/userinfo"
    oidc_userinfo_cache_ttl: 300  # Cache for 5 minutes (default: 300)
    
    # Map fields from UserInfo response
    user_id_jwt_field: "sub"
    user_email_jwt_field: "email"
    user_roles_jwt_field: "roles"
```

### Flow Diagram

```mermaid
sequenceDiagram
    participant Client
    participant LiteLLM
    participant IdP as Identity Provider

    Client->>LiteLLM: Request with Bearer token
    Note over LiteLLM: Check cache for UserInfo
    
    LiteLLM->>IdP: GET /userinfo (if not cached)<br/>Authorization: Bearer {token}
    IdP-->>LiteLLM: User data (sub, email, roles)
    
    Note over LiteLLM: Cache response (TTL: 5min)<br/>Extract user_id, email, roles<br/>Perform RBAC checks
    
    LiteLLM-->>Client: Authorized/Denied
```

### Example: Azure AD

```yaml title="config.yaml" showLineNumbers
litellm_jwtauth:
  oidc_userinfo_enabled: true
  oidc_userinfo_endpoint: "https://graph.microsoft.com/oidc/userinfo"
  user_id_jwt_field: "sub"
  user_email_jwt_field: "email"
```

### Example: Keycloak

```yaml title="config.yaml" showLineNumbers
litellm_jwtauth:
  oidc_userinfo_enabled: true
  oidc_userinfo_endpoint: "https://keycloak.example.com/realms/your-realm/protocol/openid-connect/userinfo"
  user_id_jwt_field: "sub"
  user_roles_jwt_field: "resource_access.your-client.roles"
```

## Route JWT-Shaped Machine Tokens to OAuth2

Use this when:
- `enable_jwt_auth: true` for standard JWT validation
- machine tokens are JWT-shaped and should be routed to OAuth2 based on claims

`routing_overrides` supports two operating modes:
- **Selective mode**: set `enable_oauth2_auth: false` to send only matching JWTs to OAuth2 on LLM + info routes
- **Global mode**: set `enable_oauth2_auth: true` to also enable OAuth2 on LLM + info routes

```yaml title="config.yaml"
general_settings:
  enable_jwt_auth: true
  enable_oauth2_auth: false
  litellm_jwtauth:
    user_id_jwt_field: "sub"
    routing_overrides:
      - iss: "machine-issuer.example.com"
        client_id: "MID_LITELLM"
        path: "oauth2"
```

### Matching behavior

- A rule matches when **all** configured selectors match the corresponding token claims (AND semantics).
- Supported selectors: `iss` (required), `client_id` (optional), `scope` (optional), `aud` (optional).
- Selector values can be a single string or a list of strings (the claim must match at least one entry, using the rules below).
- **Wildcards:** selectors may use shell-style `*` and `?`. Matching is **case-sensitive**, so use the same casing your IdP emits in JWT claims.
- **`scope` claim as a space-delimited string:** OAuth/OIDC often sends `scope` as one string (e.g. `openid profile App:LiteLLM`). LiteLLM splits that string **only when matching the `scope` selector**, so a configured value like `App:LiteLLM` can match. **`iss`, `aud`, and `client_id` are never split on spaces**; the full claim string is used (routing uses unverified claims only for path selection; final auth still validates the token).
- If no rule matches, LiteLLM continues with standard JWT validation.

### Example: `scope` and wildcard `client_id`

```yaml title="config.yaml"
general_settings:
  enable_jwt_auth: true
  enable_oauth2_auth: false
  litellm_jwtauth:
    routing_overrides:
      - iss: "machine-issuer.example.com"
        scope: "App:LiteLLM"
        client_id: "*MID_LITELLM"
        path: "oauth2"
```

### List-based override example

```yaml title="config.yaml"
general_settings:
  enable_jwt_auth: true
  enable_oauth2_auth: false
  litellm_jwtauth:
    routing_overrides:
      - iss: ["machine-issuer.example.com", "backup-issuer.example.com"]
        client_id: ["MID_LITELLM", "MID_BACKUP"]
        aud: ["api://litellm", "api://fallback"]
        path: "oauth2"
```

## [BETA] Control Access with OIDC Roles

Allow JWT tokens with supported roles to access the proxy.

Let users and teams access the proxy, without needing to add them to the DB.

Very important, set `enforce_rbac: true` to ensure that the RBAC system is enabled.

**Note:** This is in beta and might change unexpectedly.

```yaml
general_settings:
  enable_jwt_auth: True
  litellm_jwtauth:
    object_id_jwt_field: "oid" # can be either user / team, inferred from the role mapping
    roles_jwt_field: "roles"
    role_mappings:
      - role: litellm.api.consumer
        internal_role: "team"
    enforce_rbac: true # 👈 VERY IMPORTANT

  role_permissions: # default model + endpoint permissions for a role. 
    - role: team
      models: ["anthropic-claude"]
      routes: ["/v1/chat/completions"]

environment_variables:
  JWT_AUDIENCE: "api://LiteLLM_Proxy" # ensures audience is validated
```

- `object_id_jwt_field`: The field in the JWT token that contains the object id. This id can be either a user id or a team id. Use this instead of `user_id_jwt_field` and `team_id_jwt_field`. If the same field could be both. **Supports dot notation** for nested claims (e.g., `"profile.object_id"`).

- `roles_jwt_field`: The field in the JWT token that contains the roles. This field is a list of roles that the user has. **Supports dot notation** for nested fields - e.g., `resource_access.litellm-test-client-id.roles`.

**Additional JWT Field Configuration Options:**

- `team_ids_jwt_field`: Field containing team IDs (as a list). **Supports dot notation** (e.g., `"groups"`, `"teams.ids"`).
- `user_email_jwt_field`: Field containing user email. **Supports dot notation** (e.g., `"email"`, `"user.email"`).
- `end_user_id_jwt_field`: Field containing end-user ID for cost tracking. **Supports dot notation** (e.g., `"customer_id"`, `"customer.id"`). When set, the end-user ID from the verified JWT claim takes precedence over any request-supplied value (headers like `x-litellm-end-user-id` or body fields like `metadata.user_id`; see the [customer ID priority order](customers#1-make-llm-api-call-w-customer-id)).

- `role_mappings`: A list of role mappings. Map the received role in the JWT token to an internal role on LiteLLM.

- `JWT_AUDIENCE`: The audience of the JWT token. This is used to validate the audience of the JWT token. Set via an environment variable.

### Example Token 

```bash
{
  "aud": "api://LiteLLM_Proxy",
  "oid": "eec236bd-0135-4b28-9354-8fc4032d543e",
  "roles": ["litellm.api.consumer"] 
}
```

### Role Mapping Spec 

- `role`: The expected role in the JWT token. 
- `internal_role`: The internal role on LiteLLM that will be used to control access. 

Supported internal roles:
- `team`: Team object will be used for RBAC spend tracking. Use this for tracking spend for a 'use case'. 
- `internal_user`: User object will be used for RBAC spend tracking. Use this for tracking spend for an 'individual user'.
- `proxy_admin`: Proxy admin will be used for RBAC spend tracking. Use this for granting admin access to a token.

### [Architecture Diagram (Control Model Access)](./jwt_auth_arch)

## [BETA] Control Model Access with Scopes

Control which models a JWT can access. Set `enforce_scope_based_access: true` to enforce scope-based access control.

### 1. Setup config.yaml with scope mappings.

```yaml
model_list:
  - model_name: anthropic-claude
    litellm_params:
      model: anthropic/claude-sonnet-5
      api_key: os.environ/ANTHROPIC_API_KEY
  - model_name: gpt-3.5-turbo-testing
    litellm_params:
      model: gpt-5.6-luna
      api_key: os.environ/OPENAI_API_KEY

general_settings:
  enable_jwt_auth: True
  litellm_jwtauth:
    team_id_jwt_field: "client_id" # 👈 set the field in the JWT token that contains the team id
    team_id_upsert: true # 👈 upsert the team to db, if team id is not found in db
    scope_mappings:
      - scope: litellm.api.consumer
        models: ["anthropic-claude"]
      - scope: litellm.api.gpt_3_5_turbo
        models: ["gpt-3.5-turbo-testing"]
    enforce_scope_based_access: true # 👈 enforce scope-based access control
    enforce_rbac: true # 👈 enforces only a Team/User/ProxyAdmin can access the proxy.
```

#### Scope Mapping Spec 

- `scope`: The scope to be used for the JWT token.
- `models`: The models that the JWT token can access. Value is the `model_name` in `model_list`. Note: Wildcard routes are not currently supported.

### 2. Create a JWT with the correct scopes.

Expected Token:

```bash
{
  "scope": ["litellm.api.consumer", "litellm.api.gpt_3_5_turbo"] # can be a list or a space-separated string
}
```

### 3. Test the flow.

```bash
curl -L -X POST 'http://0.0.0.0:4000/v1/chat/completions' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer eyJhbGci...' \
-d '{
  "model": "gpt-3.5-turbo-testing",
  "messages": [
    {
      "role": "user",
      "content": "Hey, how'\''s it going 1234?"
    }
  ]
}'
```

## [BETA] Sync User Roles and Teams with IDP

Automatically sync user roles and team memberships from your Identity Provider (IDP) to LiteLLM's database. This ensures that user permissions and team memberships in LiteLLM stay in sync with your IDP.

**Note:** This is in beta and might change unexpectedly.

### Use Cases

- **Role Synchronization**: Automatically update user roles in LiteLLM when they change in your IDP
- **Team Membership Sync**: Keep team memberships in sync between your IDP and LiteLLM
- **Centralized Access Management**: Manage all user permissions through your IDP while maintaining LiteLLM functionality

### Setup

#### 1. Configure JWT Role Mapping

Map roles from your JWT token to LiteLLM user roles:

```yaml
general_settings:
  enable_jwt_auth: True
  litellm_jwtauth:
    user_id_jwt_field: "sub"
    team_ids_jwt_field: "groups"
    roles_jwt_field: "roles"
    user_id_upsert: true
    sync_user_role_and_teams: true # 👈 Enable sync functionality
    jwt_litellm_role_map: # 👈 Map JWT roles to LiteLLM roles
      - jwt_role: "ADMIN"
        litellm_role: "proxy_admin"
      - jwt_role: "USER"
        litellm_role: "internal_user"
      - jwt_role: "VIEWER"
        litellm_role: "internal_user"
```

#### 2. JWT Role Mapping Spec

- `jwt_role`: The role name as it appears in your JWT token. Supports wildcard patterns using `fnmatch` (e.g., `"ADMIN_*"` matches `"ADMIN_READ"`, `"ADMIN_WRITE"`, etc.)
- `litellm_role`: The corresponding LiteLLM user role

**Supported LiteLLM Roles:**
- `proxy_admin`: Full administrative access
- `internal_user`: Standard user access
- `internal_user_view_only`: Read-only access

#### 3. Example JWT Token

```json
{
  "sub": "user-123",
  "roles": ["ADMIN"],
  "groups": ["team-alpha", "team-beta"],
  "iat": 1234567890,
  "exp": 1234567890
}
```

### How It Works

When a user makes a request with a JWT token:

1. **Role Sync**: 
   - LiteLLM checks if the user's role in the JWT matches their role in the database
   - If different, the user's role is updated in LiteLLM's database
   - Uses the `jwt_litellm_role_map` to convert JWT roles to LiteLLM roles

2. **Team Membership Sync**:
   - Compares team memberships from the JWT token with the user's current teams in LiteLLM
   - Adds the user to new teams found in the JWT
   - Removes the user from teams not present in the JWT

3. **Database Updates**:
   - Updates happen automatically during the authentication process
   - No manual intervention required

### Configuration Options

```yaml
general_settings:
  enable_jwt_auth: True
  litellm_jwtauth:
    # Required fields
    user_id_jwt_field: "sub"
    team_ids_jwt_field: "groups"
    roles_jwt_field: "roles"
    
    # Sync configuration
    sync_user_role_and_teams: true
    user_id_upsert: true
    
    # Role mapping
    jwt_litellm_role_map:
      - jwt_role: "AI_ADMIN_*"  # Wildcard pattern
        litellm_role: "proxy_admin"
      - jwt_role: "AI_USER"
        litellm_role: "internal_user"
```

### Important Notes

- **Performance**: Sync operations happen during authentication, which may add slight latency
- **Database Access**: Requires database access for user and team updates
- **Team Creation**: Teams mentioned in JWT tokens must exist in LiteLLM before sync can assign users to them
- **Wildcard Support**: JWT role patterns support wildcard matching using `fnmatch`

### Testing the Sync Feature

1. **Create a test user with initial role**:

```bash
curl -X POST 'http://0.0.0.0:4000/user/new' \
-H 'Authorization: Bearer <PROXY_MASTER_KEY>' \
-H 'Content-Type: application/json' \
-d '{
    "user_id": "user-123",
    "user_role": "internal_user"
}'
```

2. **Make a request with JWT containing different role**:

```bash
curl -X POST 'http://0.0.0.0:4000/v1/chat/completions' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <JWT_WITH_ADMIN_ROLE>' \
-d '{
  "model": "claude-sonnet-5",
  "messages": [{"role": "user", "content": "Hello"}]
}'
```

3. **Verify the role was updated**:

```bash
curl -X GET 'http://0.0.0.0:4000/user/info?user_id=user-123' \
-H 'Authorization: Bearer <PROXY_MASTER_KEY>'
```

## [BETA] JWT-to-Virtual-Key Mapping

Map JWT identities to LiteLLM virtual keys so that JWT-authenticated users get per-user budgets, rate limits, model access controls, and spend tracking.

When a JWT comes in, LiteLLM looks up a configured claim (e.g. `email`, `sub`) in a mapping table. If a mapping exists, the request is treated as if it arrived with the corresponding virtual key, and all virtual key features apply.

### Setup

Add `virtual_key_claim_field` to your JWT auth config:

```yaml
general_settings:
  enable_jwt_auth: True
  litellm_jwtauth:
    virtual_key_claim_field: "email"         # JWT claim to look up (supports dot notation)
    virtual_key_mapping_cache_ttl: 300       # Cache TTL in seconds (default: 300)
```

### Managing Mappings

All endpoints require admin auth (`Authorization: Bearer <master_key>`).

**Create a mapping.** Link a JWT claim value to an existing virtual key:

```bash
curl -X POST http://localhost:4000/jwt/key/mapping/new \
  -H "Authorization: Bearer $LITELLM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "jwt_claim_name": "email",
    "jwt_claim_value": "user@example.com",
    "key": "sk-virtual-key-from-key-generate"
  }'
```

**List mappings** (paginated):

```bash
curl http://localhost:4000/jwt/key/mapping/list?page=1&size=50 \
  -H "Authorization: Bearer $LITELLM_API_KEY"
```

**Get a specific mapping:**

```bash
curl "http://localhost:4000/jwt/key/mapping/info?id=<mapping-id>" \
  -H "Authorization: Bearer $LITELLM_API_KEY"
```

**Update a mapping:**

```bash
curl -X POST http://localhost:4000/jwt/key/mapping/update \
  -H "Authorization: Bearer $LITELLM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "<mapping-id>",
    "description": "Updated description",
    "is_active": true
  }'
```

**Delete a mapping:**

```bash
curl -X POST http://localhost:4000/jwt/key/mapping/delete \
  -H "Authorization: Bearer $LITELLM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"id": "<mapping-id>"}'
```

### How It Works

1. A request arrives with a JWT bearer token
2. LiteLLM validates the JWT signature
3. Extracts the configured claim (e.g. `email` → `user@example.com`)
4. Looks up the claim value in the `LiteLLM_JWTKeyMapping` table
5. If a mapping exists, the request proceeds as if the mapped virtual key was used, so budgets, rate limits, model access, and spend tracking all apply
6. If no mapping exists, falls back to standard JWT auth (team-level controls)

### Error Codes

| Code | Meaning |
|------|---------|
| 409 | Duplicate mapping — a mapping for that claim name + value already exists |
| 400 | The provided key does not match an existing virtual key |
| 404 | Mapping not found (for update/delete/info) |
| 403 | Non-admin user attempted a mapping operation |

## Bind JWTs to Registered Agents

Provision agent identities in your identity provider (for example, one Microsoft Entra ID app registration per agent) and let LiteLLM enforce the agent's policies on every JWT that agent presents. When `agent_id_jwt_field` is set, LiteLLM reads that claim from the verified token, matches it against a registered agent (first by `agent_id`, then by `agent_name`), and sets `agent_id` on the authenticated identity. Everything that already applies to a virtual key bound to an agent then applies to the JWT caller too: `require_trace_id_on_calls_by_agent`, per-agent MCP server and tool restrictions, and `agent_id` spend attribution.

### Setup

Register the agent under the name your identity provider will send. For an Entra app token that is the client id, which Entra puts in the `azp` claim (v2 tokens) or `appid` (v1 tokens).

```yaml
agents:
  - agent_name: 2f5c9b1e-6a4d-4c8e-9f0b-7d1a3e5c9b21   # Entra client id of the agent's app registration
    agent_card_params:
      name: research-agent
      url: http://localhost:9999/a2a
      version: "1.0.0"
    litellm_params:
      require_trace_id_on_calls_by_agent: true

general_settings:
  enable_jwt_auth: True
  litellm_jwtauth:
    agent_id_jwt_field: "azp"   # supports dot notation for nested claims
    team_id_jwt_field: "team"
    user_id_jwt_field: "sub"
```

### Behavior

A token whose claim matches a registered agent is bound to that agent and inherits its restrictions, so with the config above a call without `x-litellm-trace-id` is rejected with `400`. A token whose claim matches no registered agent is rejected with `403` rather than falling back to an unbound identity, the same way an unknown `team_id_jwt_field` value is rejected; this applies to admin-scoped tokens as well. A token that does not carry the claim at all is authenticated exactly as before. When `agent_id_jwt_field` is unset nothing changes.

Every token that carries the configured claim is treated as an agent. Entra puts `azp` on delegated (user) tokens too, so if humans and agents obtain tokens for the same audience, `azp` will bind or reject the human callers as well. In that setup point `agent_id_jwt_field` at a claim that only agent tokens carry, such as an optional claim or a custom claim added through a claims mapping policy on the agents' app registrations, and leave `azp` for a proxy whose JWT callers are all agents.

If the token also maps to a virtual key through [JWT-to-Virtual-Key Mapping](#beta-jwt-to-virtual-key-mapping), the mapped key's own `agent_id` is used and `agent_id_jwt_field` is not consulted for that request.

## All JWT Params

[**See Code**](https://github.com/BerriAI/litellm/blob/b204f0c01c703317d812a1553363ab0cb989d5b6/litellm/proxy/_types.py#L95)

## Related pages

- [Virtual Keys](https://docs.litellm.ai/docs/proxy/virtual_keys.md)
- [JWT → Virtual Key Mapping](https://docs.litellm.ai/docs/proxy/jwt_key_mapping.md)
