mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-10 22:41:41 +00:00
docs: JWT → Virtual Key Mapping guide
This commit is contained in:
parent
4b5ca49cd2
commit
83546cc57d
1 changed files with 305 additions and 0 deletions
305
docs/my-website/docs/proxy/jwt_key_mapping.md
Normal file
305
docs/my-website/docs/proxy/jwt_key_mapping.md
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
# JWT → Virtual Key Mapping
|
||||
|
||||
:::info Enterprise
|
||||
|
||||
JWT → Virtual Key Mapping is an Enterprise feature.
|
||||
|
||||
[Get a free trial](https://enterprise.litellm.ai/demo)
|
||||
|
||||
:::
|
||||
|
||||
Map JWT tokens to LiteLLM virtual keys — so every JWT client gets the same granular controls as a virtual key: model restrictions, spend limits, rate limits, guardrails, and full spend tracking.
|
||||
|
||||
**Why this matters:** Standard JWT auth maps a JWT to a *team*. That's a shared boundary — all clients under a team share the same limits. With JWT → Virtual Key Mapping, each individual JWT client (identified by a claim like `client_id`, `azp`, or `sub`) maps to its own virtual key. You get per-client accountability without issuing API keys to your users.
|
||||
|
||||
**Common use case:** Your company uses SSO/OIDC. Developers use Claude Code with their identity tokens. You want to enforce per-developer model access and spend limits without giving each person a LiteLLM API key.
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
When a JWT arrives at the proxy:
|
||||
|
||||
1. LiteLLM validates the JWT signature against your OIDC provider.
|
||||
2. It extracts a configurable claim (e.g. `client_id`) from the token.
|
||||
3. It looks up that claim value in the JWT key mapping table.
|
||||
4. If a mapping exists, the request proceeds under that virtual key — same permission checks, spend tracking, and rate limiting as if the user had sent `Authorization: Bearer sk-...`.
|
||||
|
||||
```
|
||||
JWT Token LiteLLM Proxy Virtual Key
|
||||
───────────────────── ────────────────────── ──────────────────────
|
||||
{ 1. Validate signature sk-abc123
|
||||
"client_id": "dev-alice", 2. Extract client_id ────▶ models: ["claude-*"]
|
||||
"sub": "alice@corp.com", 3. Look up mapping max_budget: $50/month
|
||||
... 4. Apply virtual key rpm_limit: 100
|
||||
} permissions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Complete [OIDC JWT Auth setup](./token_auth.md) first — you need `JWT_PUBLIC_KEY_URL` configured and `enable_jwt_auth: True` in your proxy config.
|
||||
|
||||
### Step 1. Configure the JWT claim to map on
|
||||
|
||||
Add `jwt_client_id_field` to your `litellm_jwtauth` config. This is the JWT claim LiteLLM uses as the lookup key:
|
||||
|
||||
```yaml
|
||||
general_settings:
|
||||
master_key: sk-1234
|
||||
enable_jwt_auth: True
|
||||
litellm_jwtauth:
|
||||
team_id_jwt_field: "team_id" # existing team mapping (optional)
|
||||
user_id_jwt_field: "sub"
|
||||
jwt_client_id_field: "client_id" # 👈 claim used for key mapping
|
||||
unregistered_jwt_client_behavior: "fallback_team_mapping" # see below
|
||||
```
|
||||
|
||||
**`unregistered_jwt_client_behavior`** controls what happens when a JWT has no registered mapping:
|
||||
|
||||
| Value | Behavior |
|
||||
|-------|----------|
|
||||
| `fallback_team_mapping` | Fall through to team-based JWT auth (default — backward compatible) |
|
||||
| `reject` | Return 403 if no mapping found |
|
||||
| `auto_register` | Auto-create a virtual key + mapping on first encounter |
|
||||
|
||||
### Step 2. Register a JWT client → virtual key mapping
|
||||
|
||||
**Option A: Single call (creates key + mapping atomically)**
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://0.0.0.0:4000/jwt_client/new' \
|
||||
-H 'Authorization: Bearer <PROXY_MASTER_KEY>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"jwt_claim_name": "client_id",
|
||||
"jwt_claim_value": "dev-alice",
|
||||
"models": ["claude-sonnet-4-5", "claude-haiku-4-5"],
|
||||
"max_budget": 50.0,
|
||||
"budget_duration": "30d",
|
||||
"rpm_limit": 100,
|
||||
"tpm_limit": 50000,
|
||||
"team_id": "engineering"
|
||||
}'
|
||||
```
|
||||
|
||||
Response includes the virtual key token (only shown on creation):
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "sk-abc123...",
|
||||
"key_id": "key_123",
|
||||
"mapping_id": "mapping_456",
|
||||
"jwt_claim_name": "client_id",
|
||||
"jwt_claim_value": "dev-alice"
|
||||
}
|
||||
```
|
||||
|
||||
**Option B: Map an existing virtual key**
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://0.0.0.0:4000/jwt/key/mapping/new' \
|
||||
-H 'Authorization: Bearer <PROXY_MASTER_KEY>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"jwt_claim_name": "client_id",
|
||||
"jwt_claim_value": "dev-alice",
|
||||
"virtual_key_id": "key_123"
|
||||
}'
|
||||
```
|
||||
|
||||
### Step 3. Test it
|
||||
|
||||
```bash
|
||||
# Get a JWT from your OIDC provider (must have client_id: dev-alice)
|
||||
JWT_TOKEN="eyJhbG..."
|
||||
|
||||
curl -X POST 'http://0.0.0.0:4000/v1/chat/completions' \
|
||||
-H "Authorization: Bearer $JWT_TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"model": "claude-sonnet-4-5",
|
||||
"messages": [{"role": "user", "content": "Hello"}]
|
||||
}'
|
||||
```
|
||||
|
||||
The request is now tracked against `dev-alice`'s virtual key — spend, rate limits, and model access enforced per-client.
|
||||
|
||||
---
|
||||
|
||||
## Walkthrough: Admin grants granular access, team uses Claude Code
|
||||
|
||||
This is the full flow for an engineering team using Claude Code with company SSO.
|
||||
|
||||
### Admin setup
|
||||
|
||||
**1. Create a team for engineering**
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://0.0.0.0:4000/team/new' \
|
||||
-H 'Authorization: Bearer <MASTER_KEY>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"team_alias": "engineering",
|
||||
"models": ["claude-sonnet-4-5", "claude-haiku-4-5"]
|
||||
}'
|
||||
```
|
||||
|
||||
**2. Register each developer with their own key and spend limit**
|
||||
|
||||
```bash
|
||||
# Alice — senior eng, higher budget
|
||||
curl -X POST 'http://0.0.0.0:4000/jwt_client/new' \
|
||||
-H 'Authorization: Bearer <MASTER_KEY>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"jwt_claim_name": "client_id",
|
||||
"jwt_claim_value": "alice@corp.com",
|
||||
"team_id": "engineering",
|
||||
"models": ["claude-sonnet-4-5", "claude-haiku-4-5"],
|
||||
"max_budget": 200.0,
|
||||
"budget_duration": "30d",
|
||||
"rpm_limit": 200
|
||||
}'
|
||||
|
||||
# Bob — contractor, tighter limits
|
||||
curl -X POST 'http://0.0.0.0:4000/jwt_client/new' \
|
||||
-H 'Authorization: Bearer <MASTER_KEY>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"jwt_claim_name": "client_id",
|
||||
"jwt_claim_value": "bob@contractor.com",
|
||||
"team_id": "engineering",
|
||||
"models": ["claude-haiku-4-5"],
|
||||
"max_budget": 20.0,
|
||||
"budget_duration": "30d",
|
||||
"rpm_limit": 30
|
||||
}'
|
||||
```
|
||||
|
||||
**3. Configure Claude Code to use the proxy**
|
||||
|
||||
Set the proxy as the API base in your team's Claude Code config:
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="<your-sso-jwt-token>"
|
||||
export ANTHROPIC_BASE_URL="http://your-litellm-proxy:4000"
|
||||
```
|
||||
|
||||
Or in `~/.claude/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "http://your-litellm-proxy:4000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**4. Developers authenticate with SSO as usual**
|
||||
|
||||
When Alice runs Claude Code, her JWT (issued by your IdP with `client_id: alice@corp.com`) goes to the proxy. LiteLLM looks up the mapping, finds her virtual key, and enforces her specific limits — her $200/month budget, 200 RPM cap, and access to Sonnet and Haiku only.
|
||||
|
||||
Bob's token maps to his own key — $20/month, Haiku only, 30 RPM.
|
||||
|
||||
No API keys distributed. No shared limits. Full per-developer spend visibility in the LiteLLM dashboard.
|
||||
|
||||
---
|
||||
|
||||
## Managing mappings
|
||||
|
||||
**View a mapping + its key settings**
|
||||
|
||||
```bash
|
||||
curl 'http://0.0.0.0:4000/jwt/key/mapping/info?jwt_claim_name=client_id&jwt_claim_value=alice@corp.com' \
|
||||
-H 'Authorization: Bearer <MASTER_KEY>'
|
||||
```
|
||||
|
||||
Response includes the linked key's `models`, `max_budget`, `spend`, `rpm_limit`, `expires`, etc.
|
||||
|
||||
**Update a mapping**
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://0.0.0.0:4000/jwt_client/update' \
|
||||
-H 'Authorization: Bearer <MASTER_KEY>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"jwt_claim_name": "client_id",
|
||||
"jwt_claim_value": "alice@corp.com",
|
||||
"max_budget": 300.0
|
||||
}'
|
||||
```
|
||||
|
||||
**Delete a mapping**
|
||||
|
||||
```bash
|
||||
curl -X DELETE 'http://0.0.0.0:4000/jwt/key/mapping/delete' \
|
||||
-H 'Authorization: Bearer <MASTER_KEY>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"jwt_claim_name": "client_id",
|
||||
"jwt_claim_value": "alice@corp.com"
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
JWT-bound keys are locked down:
|
||||
|
||||
- Non-admin users cannot call `/key/update`, `/key/delete`, or `/key/regenerate` on a JWT-bound key. These return 403.
|
||||
- JWT-bound keys are automatically restricted to `llm_api_routes` — they can make LLM calls but cannot manage other keys or admin resources.
|
||||
- Only proxy admins can create, update, or delete mappings.
|
||||
|
||||
---
|
||||
|
||||
## Multi-IdP support
|
||||
|
||||
If you have users across multiple identity providers that share the same claim values (e.g. two services both have `sub: user-123` from different issuers), set `issuer` when creating the mapping:
|
||||
|
||||
```bash
|
||||
curl -X POST 'http://0.0.0.0:4000/jwt_client/new' \
|
||||
-H 'Authorization: Bearer <MASTER_KEY>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"jwt_claim_name": "sub",
|
||||
"jwt_claim_value": "user-123",
|
||||
"issuer": "https://idp-a.corp.com",
|
||||
"models": ["claude-sonnet-4-5"],
|
||||
"max_budget": 50.0
|
||||
}'
|
||||
```
|
||||
|
||||
Mappings are unique per `(claim_name, claim_value, issuer)` — so `user-123` from IdP A and `user-123` from IdP B resolve to different virtual keys.
|
||||
|
||||
---
|
||||
|
||||
## What JWT clients can and can't do vs virtual keys
|
||||
|
||||
| Capability | Virtual Key | JWT → Key Mapping |
|
||||
|---|---|---|
|
||||
| Per-client model access | ✅ | ✅ |
|
||||
| Per-client spend budget | ✅ | ✅ |
|
||||
| Per-client RPM/TPM limits | ✅ | ✅ |
|
||||
| Team membership | ✅ | ✅ |
|
||||
| Spend tracking in dashboard | ✅ | ✅ |
|
||||
| Guardrails | ✅ | ✅ |
|
||||
| Key rotation | ✅ | ✅ (admin only) |
|
||||
| Key expiry | ✅ | ✅ |
|
||||
| No API key distribution needed | ❌ | ✅ |
|
||||
| Works with existing SSO/OIDC | ❌ | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [OIDC JWT Auth](./token_auth.md) — base JWT auth setup required before using this feature
|
||||
- [Virtual Keys](./virtual_keys.md) — full virtual key documentation
|
||||
- [Access Control](./access_control.md) — model and team access control
|
||||
Loading…
Add table
Reference in a new issue