Skip to main content

Authenticate gateway requests with an external IdP

Enterprise Tier

A request to a data plane authenticates with an API key issued for the project, or carries the caller's own provider credential on the passthrough path. From 0.7.0 an application can instead present a token minted by the organization's own identity provider, verified at the gateway against a set of registered issuers. That token path is a preview: it works end to end, and a caller admitted through a gateway auth policy (from 0.9.0) or a project OIDC contract (from 0.10.0) is held to its project's budget and rate limits. Worked example of IdP token authentication applies it to one PingFederate client.


Preview: authenticating with an identity-provider token

The token path shipped in two steps. 0.6.0 stores gateway auth policies and verifies a token sent beside an API key. 0.7.0 serves a call made with a token alone, provided every data plane serving the project is on 0.7.0 or later. From 0.9.0 a caller admitted through a gateway auth policy is served as its own API key, and budgets and rate limits apply to it as they do to any key (see Budgets, rate limits, and usage). From 0.10.0 the same holds for a caller admitted through a project OIDC contract, provided the management plane and every data plane serving the project run 0.10.0 or later. A data plane older than 0.10.0 serves such a caller with no API key, so budgets do not apply to it and its rate limits do not accumulate across its requests; cap that usage at the identity provider. Worked example of IdP token authentication configures one PingFederate client end to end, with the token shape PingFederate issues; its entitlement and environment claim names are neutral examples until the customer-side claim decisions they depend on are settled. Until then, the management API reference linked below is the field list, except for the 0.10.0 contract fields identity_claim and principal_cap, which are described under The project OIDC contract and appear in the reference once it is regenerated for 0.10.0.

How it works​

Three things are configured, and one is created automatically:

ItemWhere it livesSet by
Trusted issuerAgent Router, once per identity providerPlatform operator
Claim value on a projectAgent Router, once per projectPlatform operator
Access and project scopes in the tokenThe identity provider, per applicationIdentity team
External applicationAgent Router, one per application per projectCreated on the application's first valid call

On every request, the gateway:

  1. Checks the token's signature against the trusted issuer's published keys, and checks its issuer, audience, and expiry.
  2. Reads the access claim. The token must carry exactly one, for this environment.
  3. Reads the project claim. The token must carry exactly one, and its value must be set on a project served on the hostname the request arrived on. There is no default project.
  4. Finds the external application for the issuer, the token's client ID, and that project, or creates it on the first call.
  5. Handles the request with the project's models and guardrails and the application's budget and rate limit, and records usage against the application.

The token is never forwarded to a model provider. Identity headers sent by the caller are removed before the request is handled, so a caller cannot claim to be another application. Other token fields, such as sub, iat, and jti, do not decide whether a call is accepted.

What authenticates a request at 0.7.0​

A request reaches a project through a hostname the data plane declares, and every project the data plane carries a project label for is served on every one of its hostnames. Nothing about the hostname selects the project: the credential does. Three credentials exist.

  • An API key issued for the project, on the Authorization header. The key names its project, so the request needs no other identifier, and a key from a project the data plane does not serve is refused with 403. Keys are issued by developers in the Console and governed organization-wide by operators; issuing, auditing, and revoking them is covered in Onboard developers and issue keys and Issue a project API key.
  • The caller's own provider credential, on the passthrough path: the request carries a credential for the upstream provider itself (an Anthropic subscription token, for example), and the gateway forwards rather than substitutes it. The behavior and its routing consequences are covered in Use your own provider credentials and, for the operator-side setup, Enable passthrough for Claude Code.
  • A token from the organization's own identity provider, in preview from 0.7.0, sent in the standard Authorization: Bearer header together with a header naming the project. The rest of this page describes it.

Single sign-on is a different surface: SSO authenticates people into the consoles, and its identities can then issue API keys. It never authenticates a request to a data plane.

How a token call works​

An API key names exactly one project. A token names an application, and an application may be entitled to several projects, so a token call carries one extra header naming the project the request runs as, or, from 0.10.0, relies on a claim-to-project binding to name it. The data plane takes that header as a candidate, never as a decision: the configuration the project publishes decides whether this token may use it.

Two configurations admit a token, and they are shaped for different situations.

ConfigurationWhere it is writtenWho it suits
A gateway auth policy, with identity mapping rules, on a gatewayThe GatewayAuthPolicyService of the management API, one policy per trusted issuer, up to four enabled per gatewayAn operator registering a set of issuers once for every project served on that gateway, with per-issuer key sources, audiences, and required scopes
A project OIDC contract, on one projectUpsertProjectOIDCConfig, one contract per projectA project team naming the one issuer, audience, scope, and optional entitlement its own tokens carry, and the claim that identifies one caller (see The project OIDC contract)

On either path the data plane verifies the token's signature against the issuer's key set, its expiry, and its audience, then checks what the configuration requires of its claims, and only then resolves the project. A token from an issuer the gateway does not register is refused rather than tried against another issuer's keys.

The set of trusted issuers​

One gateway carries several trusted identity providers, each with its own key source and its own accepted audiences. Every policy names its issuer, and one gateway takes at most one enabled policy per issuer, so the gateway selects the verifier from the issuer the token names and nothing else. The policy fields:

FieldWhat it does
issuerThe value the token's iss claim must carry. Required, and unique among the gateway's enabled policies.
jwks_uri or jwks_inlineWhere the signing keys come from. A jwks_uri must name the provider by a public hostname, carry no user name or password, and not resolve to an internal address. An inline key set holds at most 32 keys and 16 KiB; serve a larger set from a jwks_uri.
audiencesThe audiences a token may carry, up to eight.
required_scopesScopes the token must carry, every one of them, in its scope or scp claim, matched case-sensitively. A token missing one gets 403. Updates replace the list, so omitting it removes the requirement.
extraction_headerThe header the token is read from when it is not sent as a Bearer value. Defaults to x-tars-idp-token. Authorization is refused, as is any header the gateway cannot remove after verification, such as x-api-key or cookie. From 0.10.0, x-tars-project and x-tars-project-id are refused too, because the data plane reads either one as the request's project.
environment_claimThe claim carrying the environment the token was issued for, compared against the environment the gateway declares. An enabled policy naming one is refused while the gateway declares no environment, and the gateway refuses a token whose claim does not name its environment.

Identity mapping rules, under the same service, say which claim identifies the application and whether an application unseen before is given an identity on its first call (jit, bounded by a per-project cap) or must be pre-registered. A jit rule names the project the application is created in, and that project has to be attached to the policy's gateway before the rule is written.

Which credentials a project accepts​

Each project's membership of a gateway records accepted_credentials, written with UpdateProjectGatewayProject. A new attachment starts at ACCEPTED_CREDENTIALS_API_KEY, which is what every project did before the field existed. ACCEPTED_CREDENTIALS_BOTH admits either credential, and ACCEPTED_CREDENTIALS_TOKEN makes the data plane refuse API keys for that project. Both are refused while the project holds no enabled policy on that gateway, and the project's last enabled policy cannot be disabled or deleted while the membership declares either one. A data plane older than 0.7.0 authenticates API keys alone, whatever the membership declares.

From 0.9.0 gateway authentication also takes effect on a gateway that serves an MCP server with its own OAuth, API key, or external auth: the MCP server's routes keep their own auth, and gateway authentication covers the rest. Before 0.9.0 the two conflicted, gateway authentication applied to none of the gateway's routes, and tetrate agentrouter doctor reported it as Broken.

What an application sends​

The same base URL, endpoints, and model names as an API-key caller, plus the project header:

curl https://gw.example.com/v1/chat/completions \
-H "Authorization: Bearer $IDP_TOKEN" \
-H "x-tars-project: payments-assist" \
-d '{"model": "claude-sonnet-4-5", "messages": [{"role": "user", "content": "Hello"}]}'

The header is x-tars-project on the gateway auth policy path and X-TARS-PROJECT-ID on the project OIDC contract path; both take the project ID the management API uses. The data plane removes either header, and the token, before anything is forwarded to a model provider or an MCP server. A request that carries both a token and an x-api-key header is refused as a credentials conflict rather than guessed at.

Calls without the project header​

From data plane 0.10.0, an application on the gateway auth policy path can leave out x-tars-project when a platform administrator has bound one of its token's claim values to a project. A binding belongs to the gateway auth policy for the token's issuer and maps one claim name and one exact value, such as client_id set to payments-assist, to one project. It is written with CreateClaimProjectBinding on /admin/v1/gateway-auth-policies/{policy_id}/project-bindings, read back with ListClaimProjectBindings, and removed with DeleteClaimProjectBinding; all three need a platform grant.

curl -X POST "https://management.example.com/admin/v1/gateway-auth-policies/${POLICY_ID}/project-bindings" \
-H "Authorization: Bearer ${PLATFORM_ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"claim_name": "client_id", "claim_value": "payments-assist", "bound_project_id": "payments"}'

The request names the project in bound_project_id; the stored binding returns it as project_id. The project can be another project of the organization than the one that holds the policy, because that policy is the gateway's one verifier for the issuer. It must be attached to the policy's gateway, accept tokens there, and be covered by a rule of that policy: a jit rule naming the project, or a pre_registered rule with the principal registered in it. The management API refuses a binding on a claim that is the same for every application or changes on every token (iss, aud, exp, nbf, iat, and jti), a value that holds *, and a scope or scp value that holds a space. A policy holds at most 100 bindings, and its issuer cannot change while it holds any. Deleting a project removes the bindings that name it.

On a call with no project header, the data plane verifies the token against the policy for its issuer, then collects the projects of every binding whose value the token carries. scope and scp are read as space-separated lists, an array claim as its elements, and any other claim as one value. The call runs in the bound project only when exactly one project matches, the gateway serves that project, and the project accepts tokens there. The rest of the call is checked against the issuer's policy and its rules, for the bound project. A call that sends x-tars-project never reads a binding and is checked as before.

A binding decides which project a call runs in, not who may call. The claim a binding keys on is one the identity provider asserts and a user cannot edit, such as client_id.

On the gateway auth policy path, the first call from an application creates the application's own API key when a mapping rule allows it, and later calls reuse it. Request logs and usage are attributed to that key, which is how a token call appears in the Console today. The key is named <issuer>#<claim>=<value> after the claim its mapping rule reads, and its owner is principal-<principal ID>.

The project OIDC contract​

A contract is written for one project with UpsertProjectOIDCConfig. A caller names the project in X-TARS-PROJECT-ID. The fields:

FieldWhat it does
issuerThe identity provider that mints the project's tokens, checked against the token's iss claim. An absolute https URL.
audienceThe value the token must carry as aud, in practice the client ID of the application's registration.
required_scopeOne scope the token must carry.
required_entitlementOptional. A group the identity must belong to, matched against the groups, roles, entitlements, entitlement, group, and role claims. A claim sent as a string is one group, matched whole; a claim sent as a list is matched item by item.
enabledfalse refuses every OIDC caller and keeps the rest of the contract.
identity_claimFrom 0.10.0. The token claim whose value identifies one caller. sub unless set.
principal_capFrom 0.10.0. The number of external principals the project may hold, counted across contract callers and callers that gateway auth policy rules create in the same project. 100 unless set, with no upper bound.

A write that omits identity_claim or principal_cap keeps the stored value, and a contract written before 0.10.0 reads the defaults. A claim name that contains #, =, whitespace, or a control character, or that names a registered or group claim, is refused.

From 0.10.0 each caller admitted through a contract is served as its own API key. The management plane creates the key on the caller's first call, within principal_cap, and later calls reuse it. The key is named <issuer>#<claim>=<value>, and its owner is principal-<uuid>. A token whose identity claim is missing, not a string, blank, or oversized gets the same 403 with no detail as a project the token has no relationship with.

Two settings decide who can become a caller:

  • identity_claim names a claim the identity provider never reassigns to another caller, such as client_id or azp for an application's token, and not a claim a user can change, such as email. Every token with the same value shares one key, one budget, and one rate limit counter.
  • required_entitlement is set on every contract. Without it, any token from the issuer that carries the audience and the scope creates callers, up to principal_cap.

A project with more than 100 callers needs principal_cap raised before the extra callers are admitted. A new caller past the cap is refused with principal_cap_reached, and an existing caller's key keeps working.

The management plane refuses a contract write in these cases:

  • The deployment is Agent Router Service. Reading and deleting a contract still work there.
  • The contract is being enabled while a data plane serving the project reports no build, a build it cannot read, or a build older than 0.10.0. A project with no serving data plane is accepted. Attaching a project that has an enabled contract to a data plane, or to a gateway, is refused on the same condition.

The check does not cover a data plane created with the project already set, a data plane registered through workspace creation, the default-gateway backfill, or the data plane label path after the contract was enabled, or a data plane downgraded after the enable. Such a data plane is upgraded to 0.10.0 before it serves the project, because an older data plane serves the contract's callers with no API key.

Budgets, rate limits, and usage​

From 0.9.0 a call admitted through a gateway auth policy is held to the same controls as any call made with the application's API key, and from 0.10.0 so is a call admitted through a project OIDC contract. The budget and rate limit behavior needs the data plane on 0.9.0 (0.10.0 for a contract caller); the usage fields and the key operations need the management plane on 0.9.0 (0.10.0 for a contract caller).

ControlHow it applies to an applicationNeeds
Project budgetAn exhausted project budget stops the application's key, or moves it to the budget's fallback model, from its first call. A budget set on the application's own key applies as well once the key is listed in the next budget refresh. A refusal is 429 with the code budget_exceeded.Data plane 0.9.0
Rate limitsThe project and system per-hour token limits hold the application from its first call. Every limit that covers the key is counted once per key, whichever route the call takes. A refusal is 429 with the code rate_limit_exceeded and names the policy that fired, as described in The rate limit that refused a request.Data plane 0.9.0
UsageEach request log carries caller_type: idp for an application's key and api_key for every other caller. QueryRequestLogs and GetRequestLogStats filter on it, and GetRequestLogStats groups by caller_type and by application, labelling each application with its key's name. From 0.10.0, GetUsageStats (GET /v1/usage/stats) also takes caller_type, and its totals, series, distinct counts, and groups then count only the kept usage; an earlier management plane refuses the parameter on the REST path. The Console does not show these fields yet.Management plane 0.9.0; 0.10.0 for GetUsageStats

On a 0.10.0 data plane, with the management plane on 0.10.0, a caller admitted through a project OIDC contract is held to the same controls, from its first call: the project budget and every project rate limit apply, a budget or rate limit set on the caller's own key applies once the next snapshot lists the key, and request logs and usage name the key. A data plane older than 0.10.0 ignores the contract's key and serves the caller with no key: no budget applies to it, its rate limits do not accumulate across its requests, and usage does not name it as an application. A 0.10.0 data plane whose management plane was rolled back below 0.10.0 refuses the caller with 503 resolve_unavailable instead of serving it without a key.

Stop one application​

An application is stopped by disabling its API key, which leaves every other application and key of the project serving.

  1. List the project's application keys with GET /v1/api-keys, setting project_id and application_keys_only=true. Each key returned carries application: true, its id, and its <issuer>#<claim>=<value> name. The call needs a platform admin key, and a project ID is unique only within one organization, so the key's customer_id is checked before its id is used.

  2. Disable the key with its ID alone:

    curl -X PATCH https://<management API host>/v1/api-keys/<key id> \
    -H "Authorization: Bearer $ADMIN_KEY" \
    -d '{"is_active": false}'

    The call needs the admin scope, and only is_active may change on this path. An admin key bound to an organization reaches only that organization's application keys.

The application's calls are then refused with 403 key_revoked, and with identity_disabled once the gateway's and the management plane's 30-second caches expire. No replacement key is created. {"is_active": true} on the same path re-enables the key.

A caller admitted through a project OIDC contract is stopped the same way, by disabling its key, which is named <issuer>#<claim>=<value>. The data plane refuses the caller with key_revoked once its revocation list refreshes, every minute by default, so a call made before that refresh can still be served. No call is served more than 60 seconds after the key is disabled.

What a refusal means​

Each refusal carries its own reason in the error body, and the data plane records the reason in its metrics. The ones a caller is most likely to meet:

StatusReasonMeaning
400missing_project or project_header_requiredThe request carries a token and no project header, and the policy for the token's issuer holds no claim-to-project binding.
400credentials_conflictThe request carries both a Bearer token and an x-api-key header.
401unregistered_issuerThe token's iss names an issuer this gateway does not register.
401bad_signature, expired, wrong_audienceThe token did not verify against the issuer's keys, has expired, or was not issued for an audience the gateway accepts.
403project_not_servedThe gateway does not serve the project the header names.
403token_not_acceptedThe project named accepts API keys only on this gateway.
403issuer_not_acceptedThe issuer is registered on the gateway, but the project named does not accept tokens from it.
403missing_scope or insufficient_scopeThe token lacks a scope the policy or contract requires.
403environment_mismatch, environment_claim_missing, environment_undeclaredThe token's environment claim does not name the gateway's environment, the token carries no environment claim, or the gateway declares no environment.
403no_project_bindingThe request has no project header, and no binding on the issuer's policy matches the token.
403ambiguous_project_bindingBindings for more than one project match the token. Name the project in x-tars-project.
403bound_project_not_servedThe gateway does not serve the project the matching binding names.
403bound_project_token_not_acceptedThe project the matching binding names accepts API keys only on this gateway.
503bindings_unavailableThe gateway's auth configuration passed its size cap, so the data plane received the issuer's policy without its bindings. Fewer or shorter bindings clear it; a call that sends x-tars-project still works.
503keys_unavailableThe gateway does not hold the issuer's keys right now. Retry.
503gateway_auth_reloadingThe call arrived just after an administrator changed a gateway auth policy, and the connection it used was still draining. The response carries x-should-retry: true, and a retry succeeds on the new configuration. Before 0.10.0 the same call got 401 upstream_authentication_error. Needs a 0.10.0 data plane.
403extraction_header_conflictThe issuer's policy reads its token from a header the gateway cannot use, such as x-tars-project. Move the policy to x-tars-idp-token or another dedicated header.
403principal_cap_reachedA new caller would pass the cap: principal_cap on a project OIDC contract, or jit_principal_cap on an identity mapping rule. The existing callers keep working.
503resolve_unavailableA project OIDC contract caller reached a 0.10.0 data plane whose management plane was rolled back below 0.10.0. The refusal is not cached, and the caller is not served without a key.
403key_revokedThe application's API key has been revoked or disabled.
403identity_disabledThe application is disabled, or its API key is deleted, disabled, revoked, or expired.

A 403 on the project contract path that names nothing is deliberate: a project a token has no relationship with confirms neither that the project exists nor whose token this is. A refusal on the project OIDC contract path carries a reference_id to quote to support. A refusal on the gateway auth policy path carries its reason in code and no reference_id; the X-Request-ID response header identifies it.

What is refused when a policy is written​

The management API refuses a gateway auth policy write, with the reason in the response, when:

  • jwks_uri carries a user name or password, writes an IPv4 address as anything other than a dotted quad, or names or resolves to a carrier-grade NAT, multicast, reserved, or other internal address;
  • jwks_inline holds more than 32 keys or more than 16 KiB;
  • extraction_header is one the gateway cannot remove after verifying the token, which rules out the identity headers the gateway sets and credential headers such as x-api-key, cookie, and proxy-authorization, and from 0.10.0 the project headers x-tars-project and x-tars-project-id;
  • an identity mapping rule names a jit_project_id that is not a project of the policy's organization or is not attached to the policy's gateway, sets jit_principal_cap on a pre_registered rule, or sets a principal_type other than external on a jit rule;
  • the gateway already carries four enabled policies, or another enabled policy names the same issuer.

An unrelated update does not recheck an unchanged stored value, so an existing policy keeps working. An enabled policy that still reads its token from a header the gateway cannot strip produces the warning gateway auth policy reads its token from a header the gateway cannot keep for verification and remove before the upstream in the data plane and management plane logs; move each policy it names to x-tars-idp-token or another dedicated header.

From 0.10.0 the same warning carries the reason project_header for a policy stored with x-tars-project or x-tars-project-id as its extraction header. Such a policy never verified a token sent in that header, because the data plane reads the header as the project, and it also strips the project header from token calls to the other gateways on the same data plane. A 0.10.0 data plane refuses every token call that policy verifies with 403 extraction_header_conflict, including calls that send the token in Authorization, so the policy is moved to a dedicated header before the data plane is upgraded.

The project OIDC contract has one refusal of its own: the management plane reads the issuer's discovery document when the contract is written, and refuses an issuer on a loopback, link-local, private, carrier-grade NAT, unique-local, or otherwise non-public address. An identity provider inside the organization's own network, such as a Keycloak install, needs OIDC_DISCOVERY_ALLOW_PRIVATE_ADDRESSES=true on the management plane before its contract is written. From 0.10.0 the same setting also covers a discovery document whose jwks_uri names a non-public address, which is otherwise refused.

From data plane 0.10.0, the data plane fetches the contract's signing keys under the same rules:

  • It refuses a jwks_uri on a non-public address unless OIDC_JWKS_ALLOW_PRIVATE_ADDRESSES=true is set on the gateway's Envoy pods, and the setting takes effect when the pods restart.
  • It connects directly and ignores the HTTPS_PROXY of its environment, so a data plane that reaches the identity provider only through a proxy refuses the contract's callers until it can reach the jwks_uri directly.
  • It follows no redirect and stops reading a key set larger than 256 KiB.

An identity provider inside the organization's own network therefore needs both settings, one on each plane, or the project's OIDC callers are refused.

Watch refused token calls​

A token call that the data plane's own token check refuses writes no request log, so that refusal does not appear in the Console. A call the check admitted and the gateway's JWT filter then refused does write one, with status 401. The router_token_calls_total counter counts every token call and records the reason for each refusal, and from 0.10.0 the egress chart can render Prometheus alert rules over it.

Alert rules are off by default, because the chart does not assume the Prometheus Operator CRDs. They are turned on with the chart value tarsDataplane.alerts.tokenCalls.enabled, which is egressResources.tarsDataplane.alerts.tokenCalls.enabled through serve-helm. The Prometheus Operator CRDs must be installed. The chart renders one PrometheusRule with these settings:

ValueWhat it does
labelsExtra labels on the rule, so a Prometheus Operator that selects rules by label picks it up, for example release: kube-prometheus-stack.
metricThe counter's name in the Prometheus that evaluates the rules. Envoy exposes it as envoy_dynamicmodulescustom_router_token_calls_total, which is the default. A backend fed through the OTLP forwarder receives router_token_calls_total, and the value is set to that name there.
unverifiableTokensPerSecondThe rate of tokens that do not verify, above which TarsTokenCallUnverifiableTokens fires. 0.1 unless set.
refusalRatio, minCallsPerSecondThe share of token calls refused, and the call rate below which TarsTokenCallRefusalRatioHigh stays quiet. 0.5 and 0.05 unless set.

Upgrading a data plane that carries a policy​

Upgrade the management plane before the data planes. From 0.7.0 the data plane removes x-tars-idp-token and every other identity header it sets from the requests a caller sends, and a data plane upgraded ahead of its management plane removes that header before the gateway can verify it on a gateway with an existing policy. The data plane upgrade guide covers the order; the headers the data plane strips are listed under request normalization.