apps/opik-documentation/documentation/fern/docs/administration/authentication/jwt.mdx
JWT (JSON Web Token) authentication enables programmatic access to Opik using externally issued tokens. This is ideal for service-to-service authentication, custom auth flows, and integration with existing JWT-based systems.
<Note> JWT authentication is available on Enterprise plans. This feature is not available in open-source deployments. [Reach out](https://www.comet.com/site/about-us/contact-us/) if you want to enable this feature for your Opik deployment. </Note>JWT authentication is best suited for:
For human users logging in interactively, consider SAML or OIDC SSO instead.
Before configuring JWT authentication, you need:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Service/ │ │ Opik │ │ JWKS │
│ Client │ │ API │ │ Endpoint │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
│ 1. Request with │ │
│ JWT in header │ │
│────────────────────>│ │
│ │ 2. Fetch keys │
│ │ (cached) │
│ │────────────────────>│
│ │<────────────────────│
│ │ │
│ │ 3. Verify JWT │
│ │ signature │
│ │ │
│ │ 4. Validate claims │
│ │ (iss, aud, sub) │
│ │ │
│ 5. API response │ │
│<────────────────────│ │
Authorization header.JWT authentication can be configured with either:
| Option | Description | Use case |
|---|---|---|
| JWKS URI | URL to fetch public keys dynamically | Recommended for most deployments |
| Static Public Key | Fixed public key for verification | On-premises deployments only |
JWKS (JSON Web Key Set) is the recommended approach as it:
Configuration:
| Field | Description | Example |
|---|---|---|
| JWKS URI | URL of your JWKS endpoint | https://auth.company.com/.well-known/jwks.json |
Requirements:
Example JWKS response:
{
"keys": [
{
"kty": "RSA",
"kid": "key-1",
"use": "sig",
"n": "0vx7agoebGcQ...",
"e": "AQAB"
}
]
}
Static public key configuration is simpler but requires manual key rotation:
| Field | Description | Example |
|---|---|---|
| Static Public Key | PEM-encoded public key | -----BEGIN PUBLIC KEY-----... |
Format:
The public key must be in PEM format:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----
Subject mapping determines how Opik identifies users from JWT claims:
| Field | Description | Options |
|---|---|---|
| Subject Mapping Type | How to interpret the subject claim | EMAIL or USER_NAME |
| Subject Claim Name | Which claim contains the subject | Default: sub |
Subject mapping types:
| Type | Description | Example claim value |
|---|---|---|
EMAIL | Subject is an email address | [email protected] |
USER_NAME | Subject is a username | jsmith |
Custom claim name:
By default, Opik reads the sub (subject) claim. If your tokens use a different claim:
{
"sub": "12345",
"email": "[email protected]",
"preferred_username": "jsmith"
}
Set Subject Claim Name to email or preferred_username to use those claims instead.
Restrict which token issuers are accepted:
| Field | Description |
|---|---|
| Allowed Issuers | List of accepted iss claim values |
Example:
https://auth.company.com
https://auth.partner.com
If configured, tokens with issuers not in this list will be rejected.
Restrict which audiences are accepted:
| Field | Description |
|---|---|
| Allowed Audiences | List of accepted aud claim values |
Example:
opik-api
https://api.opik.com
If configured, tokens without a matching audience claim will be rejected.
<Tip> **Security best practice**: Configure both allowed issuers and audiences to ensure only specifically intended tokens can access your organization. </Tip>Your JWT tokens must include:
| Claim | Description | Example |
|---|---|---|
sub (or custom) | Subject identifying the user | [email protected] |
iat | Issued at timestamp | 1704067200 |
exp | Expiration timestamp | 1704070800 |
| Claim | Description | Example |
|---|---|---|
iss | Token issuer | https://auth.company.com |
aud | Intended audience | opik-api |
When using JWKS with multiple keys:
| Header | Description | Example |
|---|---|---|
kid | Key ID matching JWKS key | key-1 |
alg | Algorithm (must match key) | RS256 |
Include the JWT in the Authorization header:
curl -X GET "https://api.opik.com/v1/projects" \
-H "Authorization: Bearer <your-jwt-token>"
Configure the Opik SDK to use JWT authentication:
<Tabs> <Tab value="python" title="Python">import opik
# Configure with JWT token
client = opik.Opik(
api_key="<your-jwt-token>",
workspace="your-workspace"
)
import { Opik } from 'opik';
const client = new Opik({
apiKey: '<your-jwt-token>',
workspace: 'your-workspace'
});
Opik caches JWKS responses to improve performance:
kid is encountered.On-premises deployments can configure caching behavior:
| Environment variable | Description | Default |
|---|---|---|
JWKS_CACHE_UPDATE_SECONDS | How often to refresh the cache | 300 (5 minutes) |
JWKS_FETCH_TIMEOUT_MS | Timeout for JWKS fetch requests | 5000 (5 seconds) |
| Issue | Possible cause | Solution |
|---|---|---|
| "Invalid token signature" | Key mismatch | Verify JWKS endpoint returns correct keys |
| "Token expired" | exp claim in the past | Issue tokens with appropriate expiration |
| "Unknown key ID" | kid not in JWKS | Ensure token kid matches a key in JWKS |
| "Invalid issuer" | iss not in allowed list | Add issuer to allowed issuers or remove restriction |
| "Invalid audience" | aud not in allowed list | Add audience to allowed audiences or remove restriction |
| "User not found" | Subject doesn't map to user | Verify subject claim contains valid email/username |
Decode and inspect your JWT token (do not use for production tokens containing secrets):
# Decode JWT header and payload (without verification)
echo "<your-jwt>" | cut -d'.' -f1 | base64 -d 2>/dev/null | jq .
echo "<your-jwt>" | cut -d'.' -f2 | base64 -d 2>/dev/null | jq .
Or use jwt.io to inspect token contents.
Test that your JWKS endpoint is accessible and returns valid JSON:
curl -s "https://auth.company.com/.well-known/jwks.json" | jq .
Verify that:
keys array.kid, kty, and algorithm-specific fields.Before integrating, verify your tokens:
kid matching a key in JWKS (if multiple keys).exp claim is in the future.