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.
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.
P0 requires the JWT-SVID format. SPIFFE also defines X509-SVID and WIT-SVID, and neither can be presented here. If your platform issues X509-SVIDs by default, or exchanges them through an internal service, you must obtain the JWT-SVID form for use with the gateway.
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_uriwhere 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-knownsegment between the origin and any issuer paththe 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_urimay be on a different host. The OAuth server fetches the discovery document and thejwks_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:
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.
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.
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.
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.
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.
Audience pattern and subject pattern are regular expressions, and they are not anchored for you. They are compiled as regular expressions and tested against the claim, so a pattern matches anywhere in the value unless you anchor it.
spiffe://example.org as a subject pattern also matches spiffe://example.org.attacker.example/agent, because the pattern is unanchored and . matches any character. Anchor both ends and escape dots:
Too permissive:
spiffe://example.orgCorrect:
^spiffe://example\.org/.+$
Glob syntax does not work. A bare * is not a wildcard here and is not a valid regular expression on its own.
What this configuration is, and is not
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.
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.
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.
The JWT-SVID's audience is the gateway hostname. The token request's audience is the specific MCP server URL, which is the gateway hostname suffixed with the /mcps/{serverId} path.
Using the bare hostname as the token request audience yields an access token the gateway's data plane rejects. Using the per-server URL as the JWT-SVID audience causes validation to fail before a token is ever issued.
So for a gateway at https://gateway.example.com serving a server with identifier gcp-compute:
The JWT-SVID must be issued with
audofhttps://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.
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
The gateway and its OAuth server must be reachable at the same URL. Splitting them across separate hosts is not supported yet, so the token endpoint and the MCP server URL share the GATEWAY_URL host above.
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
Confirm the exchange.
fetch_access_token()returns without raising. A failure here is a JWT-SVID or identity provider problem, not an MCP problem.Inspect the access token's subject. Decode the returned token. Its
subis a subject P0 derives, of the formfederated/{providerId}/subject/{jwtSvidSub}— not your workload's SPIFFE ID. Confirm theproviderIdnames the identity provider you enrolled in step 3. The SVID's ownsubappears in the last{jwtSvidSub}segment.Confirm the session.
list_tools()returns the server's tools.Confirm P0 saw it. The
list_toolscall appears in the gateway's activity atGET /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:
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.
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/....
Related
Connect an MCP client: the user-delegated alternative to this guide. It also covers configuring the MCP server, which both paths need.
Identity provider: enroll the issuer that mints your JWT-SVIDs.
Gateway: register a gateway deployment, including its OAuth server endpoint.
Agentic Access Policies: govern what an authenticated agent may do.
SPIFFE specification: the JWT-SVID standard.
Last updated