MCP OAuth
The MCP OAuth remediation replaces a static OAuth client secret with the workload's SPIFFE identity. Today, an AI agent (an MCP client) presents the secret to your OAuth authorization server (typically part of your identity provider, such as PingFederate) and forwards the resulting access token to a Model Context Protocol (MCP) server.
After the remediation, the agent obtains a short-lived JWT-SVID (a JWT-formatted SPIFFE Verifiable Identity Document) through serverless attestation and exchanges the JWT-SVID at the authorization server through RFC 8693 token exchange. The authorization server keeps issuing the same access token it issues today, so the MCP server requires no change.
The remediation is documentation-guided: Ledger detects the secret, opens a remediation workflow, and tracks completion, while you apply the changes described on this page.
Supported Integrations
The following integrations detect OAuth client secrets held by agent platforms:
- Gemini Enterprise Agent Platform: IAM connector authentication providers that hold OAuth client-secret metadata.
- Amazon Bedrock AgentCore: Token-vault OAuth2 credential providers.
The remediation steps themselves are platform-neutral. Any workload whose platform identity is covered by a serverless attestation method can follow them.
When is this recommended?
Ledger recommends this remediation when the following conditions are satisfied:
- A scan detected an OAuth client secret that an agent uses to obtain access tokens, typically for an MCP server. The secret is long-lived, copyable, and often shared across agent instances. Whoever holds the secret can mint valid access tokens.
- Your authorization server supports RFC 8693 token exchange with an external JWT issuer. This capability is typically named token exchange, workload identity federation, or a JWT token processor.
- The agent workload can obtain a JWT-SVID through serverless attestation: The workload attests its platform identity directly to the Trust Domain Server, with no agent or sidecar. Agent platforms are managed runtimes, so serverless attestation is the realistic path. On Google Cloud, the GCP Instance Identity Token method covers Cloud Run, GKE, and Vertex AI Agent Engine workloads through their service-account identity tokens.
If your authorization server cannot federate an external JWT issuer, see Alternatives.
Before and after
Before the remediation, the agent authenticates to your authorization server with a static secret and forwards the resulting bearer token to the MCP server:
After the remediation, no static secret exists anywhere in the chain. The agent attests its platform identity to the Trust Domain Server (serverless attestation, no agent or sidecar), receives a short-lived JWT-SVID, and exchanges the JWT-SVID at the authorization server for the same access token as before:
The exchange uses the RFC 8693 token-exchange grant on your authorization server's token endpoint. Step 2 of this remediation workflow configures the authorization server's side of the exchange, and Step 3 configures the agent's side.
What does the workflow look like?
The workflow appears on the Remediations page and on the secret's detail page in the Defakto Console, with a single documentation-guided step that links to this page.
Complete the steps below, then re-run the integration scan. Ledger closes the workflow when the scan no longer finds the static credential.
Remediation steps
Step 1: Issue the agent an identity
Enroll the agent workload in a Defakto trust domain by applying a serverless attestation policy. The policy's pathTemplate defines the workload's SPIFFE ID. For a GCP workload:
section: ServerlessAttestation
schema: v1
spec:
policies:
- name: gcp-agent-serverless
svidPolicy:
pathTemplate: "/gcp/{{gcp_iit.project_id}}/{{gcp_iit.service_account.id}}"
requiredAttestors:
- type: gcp_iit
config:
allowedProjectIDs:
- your-project-id
allowNonComputeTokens: true
allowNonComputeTokens: true accepts service-account identity tokens without Compute claims, which is what serverless platforms such as Cloud Run and Vertex AI Agent Engine present.
The workload must run as a user-managed (custom) service account: The gcp_iit.service_account.id attribute is not available for a platform's default service account. Apply with spirlctl config set trust-domain --id <trust-domain-id> serverless.yaml.
Then record the trust domain's OIDC issuer and JWKS URL. Your authorization server needs both values in Step 2:
spirlctl trust-domain info example.com --output json | jq -r '.jwt_issuer.effective_issuer, .jwks_endpoint'
# e.g. https://fed.prod.spirl.org/t-acme/td-01
# https://fed.prod.spirl.org/t-acme/td-01/jwks
Finally, declare the trust domain on the agent runtime through the spiffe-defakto SDK's environment variables. The variables tell the workload which trust domain to attest to, and they let Ledger link the discovered agent to the trust domain and predict the workload's SPIFFE ID:
| Variable | Value |
|---|---|
DEFAKTO_ATTESTORS | gcp_service_account for the GCP method shown above |
DEFAKTO_TRUST_DOMAIN_ID | The trust domain ID (for example td-m36ckrte4e), for Defakto-hosted trust domains |
DEFAKTO_SERVER_ADDRESS | The Trust Domain Server address, for self-hosted deployments |
The SDK examples describe the variables in detail. Step 3's exchange code reads the same variables.
Step 2: Configure your authorization server
Configure the authorization server to accept the workload's JWT-SVID as the subject token of an RFC 8693 token exchange. The concrete objects differ per vendor, but every setup covers the same four points:
- Trust the Defakto trust domain as a token issuer. Register the OIDC issuer URL and JWKS URL from Step 1, so the authorization server validates JWT-SVID signatures against the trust domain's published keys.
- Require an exchange audience. Choose an audience value for the exchange and require it on incoming subject tokens. The agent requests the same value in its JWT-SVID in Step 3.
- Map the subject. The incoming token's
subclaim carries the workload's SPIFFE ID. Map thesubclaim into authorization decisions and audit logs. The static secret's single sharedclient_idnever distinguished agent instances. - Keep the issued token's shape. Configure the issued access token to carry the same
audandscopeas the token issued through theclient_credentialsgrant today, so the MCP server continues to work unchanged.
On PingFederate, the following objects implement the exchange:
- Enable the token exchange grant for the OAuth client that performs the exchange.
- Create a JWT Token Processor 2.0 instance that trusts the trust domain: Set the issuer and JWKS URL from Step 1, require the agreed audience under Allowed Audiences, and set the JWKS cache duration below the trust domain's key-retirement window so rotated signing keys are picked up in time.
- Create a Token Exchange Processor Policy that maps
subject_token_typeurn:ietf:params:oauth:token-type:jwtto the JWT Token Processor. - Configure the Access Token Manager mapping so the issued token reproduces today's
audandscope. - For client authentication on the exchange call, use mutual TLS with the workload's X.509-SVID, or a public client. Do not use
private_key_jwt: PingFederate requires the assertion's issuer to equal the client ID, which a JWT-SVID does not satisfy.
PingOne SaaS cannot ingest an external SVID as the subject token: The PingOne token-exchange grant only accepts PingOne's own gateway credential. For a PingOne-only environment, front the exchange with PingFederate.
Step 3: Exchange instead of authenticating
Remove the client secret from the agent's configuration and replace the client_credentials call with the two-step exchange: First fetch a JWT-SVID with the audience value chosen in Step 2, then post the JWT-SVID to the authorization server's token endpoint as an RFC 8693 token exchange:
| Parameter | Value |
|---|---|
grant_type | urn:ietf:params:oauth:grant-type:token-exchange |
subject_token | The JWT-SVID |
subject_token_type | urn:ietf:params:oauth:token-type:jwt |
scope | The same scope the agent requests today |
For a Python agent (for example one built on Google's Agent Development Kit), the spiffe-defakto SDK attests the workload's platform identity and fetches the JWT-SVID directly from the Trust Domain Server. The SDK reads the environment variables set in Step 1:
import httpx
from spiffe_defakto import WorkloadAPIClient
EXCHANGE_AUDIENCE = "mcp-token-exchange" # the audience agreed in Step 2
TOKEN_ENDPOINT = "https://as.example.com/token" # your authorization server's token endpoint
SCOPE = "mcp:invoke" # placeholder: use the scope your agent requests today
async def fetch_access_token() -> str:
# Attest the platform identity and fetch a JWT-SVID whose aud is
# the agreed exchange audience.
async with WorkloadAPIClient() as client:
svid = await client.jwt.fetch_svid(audience=[EXCHANGE_AUDIENCE])
# Exchange the JWT-SVID for an access token (RFC 8693 token exchange).
async with httpx.AsyncClient(timeout=30) as http:
resp = await http.post(
TOKEN_ENDPOINT,
data={
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token": svid.token,
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"scope": SCOPE,
},
)
resp.raise_for_status()
return resp.json()["access_token"]
The example authenticates as a public client: The JWT-SVID itself is the credential. If your authorization server requires client authentication on the token endpoint, present the workload's X.509-SVID as the TLS client certificate (mutual TLS), matching the client-authentication choice from Step 2.
Pass the token to the MCP connection as a bearer header, exactly as with the old token:
from google.adk.tools.mcp_tool import MCPToolset, StreamableHTTPConnectionParams
token = await fetch_access_token()
toolset = MCPToolset(
connection_params=StreamableHTTPConnectionParams(
url="https://mcp.example.com/mcp",
headers={"Authorization": f"Bearer {token}"},
),
)
The access token's lifetime is determined by your authorization server. Frameworks that capture headers once at construction, such as ADK MCPToolset in the example above, require the toolset rebuilt with a fresh token before it expires. Because JWT-SVIDs are short-lived by design, a new JWT-SVID must be fetched for each token exchange rather than reusing a cached one.
Step 4: Verify and retire the secret
- Call the MCP server through the new path and confirm it succeeds, with the same scopes as before.
- Disable or rotate the old client secret at the authorization server. Confirm the old path now fails and the new path still succeeds, proving that no workload silently depends on the secret.
- Delete the stored secret from the agent platform: The IAM connector authentication provider on Gemini Enterprise Agent Platform, or the token-vault OAuth2 credential provider on Amazon Bedrock AgentCore. Remove the
client_secretfrom the agent's OAuth client at the authorization server too. The token-exchange path does not use the secret. - Re-run the integration scan from the Defakto Console. Ledger closes the remediation workflow when the scan no longer detects the static credential.
Outcome
The agent holds no static credential. The authorization server mints access tokens on demand from the workload's SPIFFE identity, and the exchanged token's subject claim identifies each agent instance individually. The MCP server is untouched and continues to validate the same tokens as before.
Alternatives
You control the MCP server's token validation. The Trust Domain Server can issue the OAuth access token itself through the RFC 7523 jwt-bearer grant, and the MCP server then validates tokens against the Defakto OAuth issuer instead of your authorization server. This path is recommended when your authorization server does not support token exchange, or when you want to take the authorization server out of the runtime path entirely.
The agent acts on behalf of a human user. When downstream services need the user's identity as well as the workload's, see Delegation. Delegation issues a JWT-SVID that carries both identities; Delegation does not issue OAuth access tokens.