API Key + JWT Multi-Auth
This example shows how to accept either an API key or a JWT on the same route, using a composite inbound policy. Requests succeed if either credential is valid; unauthenticated requests get a 401.
This pattern is useful for:
- Mixed clients: Server-to-server traffic uses long-lived API keys, while end-user traffic uses short-lived JWTs from your IdP
- Migration paths: Onboard JWT-based auth without breaking existing API key consumers
- Single endpoint, multiple identities: Resolve
request.userfrom whichever credential the caller presented
Prerequisites
- A Zuplo account. You can sign up for free.
- An OIDC-compatible IdP (Auth0, Clerk, Cognito, Okta, Supabase, etc.) that issues JWTs and exposes a JWKS endpoint.
Deploy this example to Zuplo
Click the Deploy to Zuplo button anywhere on this page to create a new project in your Zuplo account with this example pre-configured.
After deploy, set the JWT environment variables in your Zuplo project (Settings → Environment Variables):
| Variable | Description |
|---|---|
JWT_ISSUER | Your IdP’s issuer URL (e.g. https://your-tenant.auth0.com/) |
JWT_AUDIENCE | Expected aud claim for tokens issued to this API |
JWT_JWKS_URL | JWKS endpoint your IdP publishes (e.g. https://your-tenant.auth0.com/.well-known/jwks.json) |
API keys are managed in the Zuplo portal under API Key Service — create a consumer and key there.
How It Works
A single composite policy (dual-auth) runs three inbound policies in order:
api-key-auth— validatesAuthorization: Bearer <api-key>. Setsrequest.userif the key matches a Zuplo-managed consumer.allowUnauthenticatedRequests: truelets the request continue to the next policy if no key is present.jwt-auth— validates a JWT against the configured issuer, audience, and JWKS URL. Setsrequest.userfrom the token claims. Also runs inallowUnauthenticatedRequests: truemode.require-auth— custom code policy that checksrequest.user.sub. If neither prior policy authenticated the caller, it returns a 401.
Whichever credential matched first populates request.user, so handlers don’t need to know which auth method was used.
Project Structure
API Endpoints
| Method | Path | Description |
|---|---|---|
GET | /me | Returns sub and data from request.user |
Testing
Replace YOUR_GATEWAY_URL with your deployed gateway’s URL.
1. No credentials → 401
2. With an API key
3. With a JWT
Both authenticated calls return:
Extending This Example
- Role-based access: Inspect
request.user.datainrequire-auth.tsand reject requests missing required scopes or roles - Per-route auth: Apply
api-key-authonly to machine routes anddual-authto user-facing routes - Additional providers: Add a second
open-id-jwt-auth-inboundpolicy for a second IdP and include it in the composite
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
401 with valid JWT | JWT_ISSUER / JWT_AUDIENCE mismatch | Decode the token at jwt.io and confirm iss and aud match your env vars exactly (trailing slash matters) |
401 with valid API key | Key not provisioned | Create the consumer + key in the Zuplo portal’s API Key Service |
500 on first request | JWT_JWKS_URL unreachable | Verify the JWKS URL returns JSON in a browser |