For the complete documentation index, see llms.txt. This page is also available as Markdown.

JWT-SVID

Enroll a SPIFFE identity provider with P0, and authenticate an unattended workload to the P0 AI Gateway with an existing JWT-SVID from Python.

This guide covers authenticating an unattended agent to the P0 AI Gateway with a SPIFFE JWT-SVID: enrolling the identity provider that issues your JWT-SVIDs, setting the audience values correctly, and presenting a JWT-SVID from Python.

The Agentic Gateway integration is available as an opt-in capability. Contact P0 to enable it for your organization.

1. Overview

When to use JWT-SVID authentication

Use JWT-SVID authentication when your agent runs as a workload rather than on behalf of a person: a service, a batch job, a pod in your cluster. There is no browser and no one to sign in, so the agent authenticates as itself using the identity your SPIFFE infrastructure already gives it.

If a person is present and the agent acts for them, use the client ID and secret path in Connect an MCP client instead.

What this guide assumes

This guide assumes your workload can already obtain a JWT-SVID. Issuing, minting, rotating, and mounting SVIDs is the job of your SPIFFE infrastructure, and is out of scope here. What follows covers only what P0 requires.

An SVID is a SPIFFE-compatible format for presenting a SPIFFE ID, the identifier for a workload. SPIFFE defines several SVID formats, each with its own conventions. In a JWT-SVID the SPIFFE ID is carried as the sub claim.

What must already exist

  • A deployed P0 AI Gateway and a registered Gateway component.

  • At least one MCP server configured behind that gateway. Note its server identifier.

  • A JWT-SVID available to your workload, and the issuer URL of whatever issues it. The issuer must serve an OpenID Connect discovery document whose issuer value matches your configured issuer exactly, and which advertises a jwks_uri where the signing keys are published. The OAuth server checks two locations for that document and accepts whichever one returns a document that declares your registered issuer:

    • the RFC 8414 inserted form, {origin}/.well-known/openid-configuration{issuer-path}, which inserts the .well-known segment between the origin and any issuer path

    • the OIDC Discovery appended form, {issuer}/.well-known/openid-configuration, which appends the segment to the full issuer

    Okta custom authorization servers and PingOne environments publish at the appended form, so an Okta-style issuer that carries a path is still supported. The jwks_uri may be on a different host. The OAuth server fetches the discovery document and the jwks_uri, so the issuer must be reachable from it over HTTPS at a publicly resolvable address. Private ranges and redirects are both rejected.

  • An issuer that signs with RS256 or ES256. These are the only algorithms a gateway accepts by default.

  • Python 3.10 or later.

2. How the authentication flow works

Four steps, of which your code performs two:

  1. Your workload obtains a JWT-SVID. Handled by your SPIFFE infrastructure, typically written to a file that a sidecar or CSI driver keeps rotated. Your code reads it.

  2. Your workload presents the JWT-SVID to P0. It posts the JWT-SVID to the gateway's OAuth server token endpoint as a JWT bearer assertion, naming the MCP server it wants to reach.

  3. P0 validates it. The OAuth server verifies the JWT-SVID's signature against the issuer's published keys, checks the JWT-SVID's audience, and asks P0 whether the issuer is enrolled and whether the token's audience and subject satisfy that enrollment's patterns.

  4. The gateway's authorization server issues an access token. On success the OAuth server returns a short-lived access token scoped to that one MCP server. Your client presents that token, not the JWT-SVID, on every MCP request.

The important consequence of step 4: the JWT-SVID is exchanged once for an access token, and the access token is what the gateway sees. These are two different tokens, and the Authorization: Bearer header on your MCP requests carries the access token.

3. Enroll the SPIFFE identity provider in P0

Enrolling an identity provider tells P0 that JWTs from a given issuer may authenticate agents to the gateway. Follow Identity provider to enroll one, then set its fields for a JWT-SVID as follows.

Field
What it must match for a JWT-SVID

Issuer

The issuer URL of your SPIFFE infrastructure, matching the JWT-SVID's iss claim. Compared for exact string equality, never as a pattern.

Audience pattern

The JWT-SVID's aud claim, which must be the gateway hostname. See step 4.

Subject pattern

The JWT-SVID's sub claim, which is the workload's SPIFFE ID, for example spiffe://example.org/ns/prod/sa/my-agent.

What this configuration is, and is not

The identity provider configuration is a coarse authentication filter, not a set of per-agent rules. It establishes that an issuer is trusted and that a token's audience and subject fall within accepted patterns. It does not say which agent may do what.

Binding a verified identity to a specific agent happens on the client registration, not here. See Register an agent identity. Authorization over tool calls lives in Agentic Access Policies. Do not attempt to restrict individual agents in the identity provider's patterns.

Whether the provider automatically registers a matching agent depends on the provider's Dynamic registration setting. With it disabled, you must register each agent identity before P0 accepts that agent's JWT-SVID.

The check is also tenant-wide. A token passes if any enrolled identity provider matches it.

To manage Agentic Gateway configuration as code, use the P0 Terraform provider's p0_agentic_gateway and p0_agentic_gateway_staged resources.

4. Configure the audience

There are two different audience values in this flow, and they're not the same. Setting both to the same value is the most common reason a correctly issued JWT-SVID is rejected.

Audience
Value
Where it is set

The JWT-SVID's own aud claim

The gateway hostname, for example https://gateway.example.com

Set by whatever issues the JWT-SVID. Must match the OAuth server's configured assertion audience, and must satisfy the Audience pattern from step 3.

The audience parameter on the token request

The per-server URL, {gatewayUrl}/mcps/{serverId}

Set by your code, in the token request in step 5.

So for a gateway at https://gateway.example.com serving a server with identifier gcp-compute:

  • The JWT-SVID must be issued with aud of https://gateway.example.com.

  • The Audience pattern in step 3 must match that, for example ^https://gateway\.example\.com$.

  • The token request must send audience=https://gateway.example.com/mcps/gcp-compute.

5. Connect from Python

The MCP SDK installs httpx, so the token request needs no extra dependency.

The following snippet targets MCP Python SDK v2, whose import paths differ from v1. In v2 the transport helper is streamable_http_client, not streamablehttp_client. Check your installed version with pip show mcp.

Reading the JWT-SVID

Read the JWT-SVID from wherever your SPIFFE infrastructure puts it, commonly a file kept rotated by a sidecar or CSI driver. Read it fresh on each exchange rather than caching it at startup, so a rotated JWT-SVID is picked up.

The client

Access tokens are short-lived, and this snippet obtains one and holds it, which is enough for a single short session. A long-running agent needs more: set the Authorization header per request rather than once on the client, so that when the gateway answers 401 you can exchange a fresh JWT-SVID and retry with the new access token. Reading the SVID file on each exchange, as read_svid() does, is what makes that possible.

6. Verify the connection

  1. Confirm the exchange. fetch_access_token() returns without raising. A failure here is a JWT-SVID or identity provider problem, not an MCP problem.

  2. Inspect the access token's subject. Decode the returned token. Its sub is a subject P0 derives, of the form federated/{providerId}/subject/{jwtSvidSub}not your workload's SPIFFE ID. Confirm the providerId names the identity provider you enrolled in step 3. The SVID's own sub appears in the last {jwtSvidSub} segment.

  3. Confirm the session. list_tools() returns the server's tools.

  4. Confirm P0 saw it. The list_tools call appears in the gateway's activity at GET /o/{orgId}/agentic/activity.

7. Troubleshooting

The token endpoint answers a failed exchange in one of three ways. Identify which before working through the table:

Response
Meaning

400 invalid_request

The request is malformed. The error_description names the problem.

400 invalid_grant

The JWT-SVID failed validation. Deliberately opaque: every check returns this same response.

503 temporarily_unavailable

The gateway could not reach P0. Nothing is wrong with your JWT-SVID — check the gateway's connectivity to P0.

Finding out which check failed

invalid_grant never says what went wrong, but the gateway runs in your own environment, so its logs do. In the OAuth server's logs, find the audit event auth.jwt_bearer_grant.assertion.outcome with outcome=denied. Its reason attribute names the exact check that failed, for example assertion_expired, assertion_lifetime_exceeded, issuer_not_registered, or signature_or_registered_key_invalid.

signature_or_registered_key_invalid is the broadest of these: it covers a bad signature, a wrong aud, an expired token, a disallowed algorithm, and every issuer-discovery failure. When you see it, the adjacent warning federated assertion signature verification failed carries the underlying error, which distinguishes them.

Work through the following table in order.

Symptom
Cause
Fix

The token is a certificate, or has no spiffe:// subject

The SVID is in X509-SVID form, not JWT-SVID form

Obtain the JWT-SVID. P0 cannot accept an X509-SVID.

invalid_grant, and the JWT-SVID's aud is the per-server URL

Wrong JWT-SVID audience

The JWT-SVID's aud must be the gateway hostname. See step 4.

Exchange succeeds, then the gateway answers 401 invalid_token on the first MCP request

Wrong token request audience

Nothing validates the audience parameter at the token endpoint, so a wrong value still returns 200 and a well-formed token. It fails only when the gateway checks the token's aud. The value must be exactly {gatewayUrl}/mcps/{serverId}, with no trailing slash. Omitting audience entirely produces a token that can never reach an MCP server.

invalid_grant, audience and issuer both look right

Subject pattern does not match the JWT-SVID's SPIFFE ID

Test the pattern against the actual sub. Remember it is a regular expression: anchor it and escape dots. An over-anchored or mistyped pattern fails silently.

invalid_grant, and the issuer looks right

The issuer is not enrolled, or does not match exactly

Issuer is compared for exact string equality against iss. A trailing slash or an http versus https mismatch fails.

invalid_grant on a freshly issued JWT-SVID

The JWT-SVID has expired, or its lifetime exceeds what the gateway accepts

Check exp. The gateway rejects assertions whose remaining lifetime is too long, so a long-lived JWT-SVID fails even before expiry. Gateways deployed with the Helm chart cap this at 300 seconds, which exactly matches SPIRE's default jwt_svid_ttl and leaves no margin. Either shorten the issuer's TTL or raise the gateway's limit above it.

invalid_grant, and everything else checks out

The issuer signs with an algorithm the gateway does not accept

Only RS256 and ES256 are accepted by default. SPIRE configured for ES384, ES512, or EdDSA fails here with no distinguishing error. Check your issuer's key type.

503 temporarily_unavailable

The gateway cannot reach P0

Not a JWT-SVID problem. Check the gateway's network path to P0 and its P0 connection settings.

invalid_request mentioning client authentication

The token request included client_id, client_secret, or a client assertion

The JWT bearer grant rejects these outright. Send only grant_type, assertion, and audience.

A 2xx response that is not JSON

Pointing at an SSO proxy or the console front end, not the OAuth server

Use the gateway's OAuth server host. A proxy answers 200 with an HTML login page.

404 on an API call

Used mcp as the integration key, or prefixed the read endpoints with integrations/

The integration key is agentic. Server and client reads live at /o/{orgId}/agentic/...; component configuration lives at /o/{orgId}/integrations/agentic/config/....

Last updated