MCP

Securing Agentic OAuth Flows with Riptides

Securing Agentic OAuth Flows with Riptides
Written By
Zsolt Rappi
Published On
Apr 8, 2026

In our previous post on SPIFFE-backed OAuth for MCP, we showed how workloads running inside a Riptides environment can use their SPIFFE identity to self-register and authenticate with OAuth, eliminating client secrets entirely. That story assumed a world where both the agent and the MCP server are managed by Riptides.

The reality is that most remote MCP servers today are third-party services: productivity tools, data providers, SaaS platforms. They run their own OAuth2 authorization servers, and agents connecting to them must go through a standard OAuth2 authorization code flow, authenticate the user, and store the resulting access token. That last part is the problem.

Access tokens are sensitive credentials. They grant delegated access to a user’s resources for as long as they remain valid. Yet in virtually every agentic framework today, these tokens end up stored as plaintext: in memory, in configuration files, in environment variables passed to subprocesses. The agent that holds the token is an attractive target.

This post describes how Riptides solves this. We transparently broker the authentication on behalf of the agent and inject the real credential at the kernel level at request time. The agent participates in a fully standard OAuth2 flow, gets back a token it can store, and sends that token with every request, but the token it holds is a Riptides-issued JWT that is worthless outside our system. The real access token never leaves the kernel.

TL;DR: When an agent connects to a remote MCP server, Riptides acts as an intermediate OAuth2 authorization server. The agent completes a standard OAuth flow and stores a Riptides-issued JWT. The real access token is brokered by Riptides, stored in the kernel, and injected into outgoing requests at the network level. The agent never holds the credential that actually grants access.

How Remote MCP Servers Use OAuth2

RFC 9728 — OAuth 2.0 Protected Resource Metadata defines a discovery mechanism that any OAuth-protected resource server can implement. A client discovers how to authenticate by fetching a well-known document:

GET /.well-known/oauth-protected-resource

The response tells the client which authorization server(s) protect this resource. The client then fetches the authorization server’s own metadata from a second well-known endpoint (RFC 8414), discovers the authorization and token endpoints, and proceeds with the OAuth2 flow.

For remote MCP servers, the MCP specification requires this exact mechanism. An MCP client:

  1. Fetches the protected resource metadata to discover the authorization server
  2. Optionally registers itself as a client via Dynamic Client Registration (RFC 7591)
  3. Runs the authorization code flow, redirecting the user to log in
  4. Receives an access token and stores it for subsequent requests

Steps 1–2 happen once per client registration. Step 3 happens once per user. Step 4 is the problem. The agent now has a token.

The Problem with Tokens in Agent Memory

Access tokens represent a user’s delegated trust. If an agent is compromised (through prompt injection, a vulnerability in the framework, or a misconfigured process), the attacker inherits that trust. They can replay the token against the MCP server directly. Nothing in the token itself proves it came from the legitimate agent process.

This is the same credential sprawl problem we see everywhere in non-human identity, in a new form. Long-lived API keys got replaced with OAuth tokens, but the storage pattern is identical: plaintext, in the workload’s memory space, accessible to anything running as that process.

The standard mitigations (short TTLs, refresh token rotation) reduce the window of exposure but don’t eliminate it. A token that lives for an hour is still a token that can be stolen and replayed for an hour.

Our Approach: Riptides as an Intermediate Authorization Server

All the building blocks to solve this already existed in Riptides. We have on-the-wire credential injection, which replaces credentials in outgoing requests at the kernel level. We have a control plane that is a fully-fledged OIDC server. We have process-level workload identity that knows exactly which agent process is making each request.

The new piece is an intermediate OAuth2 flow that connects all three together.

At a high level: the agent thinks it is talking to the MCP server’s authorization server, but it is actually talking to Riptides. It intercepts the agent’s OAuth discovery requests and rewrites the authorization server endpoints to point to the Riptides control plane. No changes to the agent or its configuration are required. Riptides brokers the real authorization flow on the agent’s behalf, acquires the real access token, and stores it in the kernel. The agent receives a Riptides-issued JWT, a proxy credential that is only valid inside our system, which it stores and sends with every request. At request time, the kernel recognizes the JWT, selects the corresponding real credential, and replaces it before the traffic leaves the machine.

The agent is a standard OAuth2 client throughout. It follows the spec. It just never sees the sensitive token.

Auth flow diagram showing Claude authenticating via Riptides, which brokers the real OAuth flow to Cloudflare MCP and stores the access token in the kernel

The Double Auth Flow, Step by Step

Here is the full sequence in detail.

1. The agent initiates the OAuth flow

When the agent begins the OAuth2 authorization code flow for the remote MCP server, it interacts with the Riptides control plane, which acts as the authorization server. The agent performs Dynamic Client Registration (RFC 7591) and, as a Riptides-managed workload, can use its SPIFFE identity as the registration credential via SPIFFE-backed registration. No pre-shared client secrets are needed.

2. The user logs in to Riptides (first authentication)

The user is redirected to Riptides’ authorization endpoint and logs in.

At this point, Riptides has assembled all the context it needs:

  • Workload identity: the SPIFFE ID of the agent process, established by the kernel module at process start
  • User identity: the authenticated user who just logged in
  • Target resource: the MCP server this agent is trying to reach

3. The real OAuth flow toward the MCP server (second authentication)

Now that Riptides knows who the user is and which MCP server they want to reach, it starts the actual OAuth2 flow toward the MCP server’s real authorization server, the one configured as a CredentialSource in the Riptides control plane.

The control plane registers itself as a client with the real authorization server and redirects the user to authenticate there. The user logs in a second time, this time to the MCP server’s own authorization server.

Riptides does not silently impersonate the user. The MCP server’s authorization server issues tokens for its own users, and the user must consent there directly.

After the user authorizes, the MCP server’s authorization server returns the access token to the Riptides control plane. This double login happens once per user. The agent receives a refresh token alongside the Riptides JWT and handles renewal through the standard OAuth2 refresh flow. The double login only recurs if the refresh token itself expires.

4. Credential storage and JWT issuance

The control plane stores the access token as a credential and propagates it to the kernel module on the agent’s host. This is the same credential lifecycle used for any other CredentialSource type in Riptides, whether that source is AWS, GCP, or OCI. The CredentialSource tells the control plane how to retrieve the credential; the CredentialBinding tells the kernel where to inject it.

If the MCP server’s authorization server also issued a refresh token, Riptides stores that too and handles renewal transparently. When the access token expires, the control plane refreshes it and propagates the updated credential to the kernel without any action required from the agent or the user.

The control plane then issues a short-lived Riptides JWT and returns it to the agent as the result of the authorization code flow. Here is what that token looks like at runtime:

{
  "sub": "spiffe://acme.corp/claude",
  "act": "spiffe://acme.corp/employee/acme.corp/alice",
  "aud": ["https://controlplane.riptides.io"],
  "client_id": "0ec59979-20cd-44a5-a3e4-715457539172",
  "iss": "https://controlplane.riptides.io/oauth2/.../oidc",
  "iat": 1775637390,
  "exp": 1775723790,
  "nbf": 1775637390,
  "jti": "85bc11fe-7a24-4542-bbdf-8a4a80c86719"
}

The sub claim is the SPIFFE identity of the agent process. The act claim (RFC 8693) carries the SPIFFE identity of the user on whose behalf the agent is acting. The audience is scoped to the Riptides control plane, not the MCP server. This token is meaningless anywhere else.

The agent stores this JWT. It looks like a normal access token. It is not.

5. Credential injection at request time

When the agent sends a request to the MCP server, it includes the Riptides JWT in the Authorization header, exactly as it would with a real access token.

The kernel module intercepts the outgoing request. It inspects the JWT, verifies the workload identity matches the process making the call, looks up the stored credential for that workload and user combination, and replaces the JWT with the real access token.

The request arrives at the MCP server fully authenticated. The agent process never held the real token. The real token never appeared in user space.

Token exchange diagram showing Claude presenting the Riptides JWT, which the kernel exchanges for the real user access token before the request reaches Cloudflare MCP

Why the JWT Is Safe to Store

A Riptides JWT has no value outside of the Riptides system. There is no endpoint at the MCP server or its authorization server that will accept it. An attacker who extracts the JWT from agent memory gains nothing; they cannot replay it against the MCP server directly.

Inside the Riptides system, the JWT is still constrained. It is bound to a specific workload identity: only requests from the process with the matching SPIFFE ID will trigger credential injection. A JWT presented by a different process will not be honored. It is bound to a specific user: the credential lookup requires both the workload and the user to match. And it is short-lived: the JWT expires, and the agent must use its refresh token to obtain a new one. A full authorization flow is only required again if the refresh token itself has expired.

This is the same security model that applies to every credential type Riptides manages. The CredentialSource retrieves the real credential; the CredentialBinding scopes where and for whom it gets injected. The workload participates in the authenticated flow, but it never possesses the credential that grants access.

Configuration

From an operator’s perspective, three resources need to be defined.

First, register the remote MCP server as a Service:

apiVersion: core.riptides.io/v1alpha1
kind: Service
metadata:
  name: cloudflare-mcp
spec:
  addresses:
    - address: mcp.cloudflare.com
      port: 443
  labels:
    app: cloudflare-mcp
  external: true

Second, configure the MCP server’s authorization server as a CredentialSource. This tells the Riptides control plane where to run the real OAuth flow:

apiVersion: core.riptides.io/v1alpha1
kind: CredentialSource
metadata:
  name: cloudflare-mcp
  namespace: riptides-system
spec:
  oa2ac:
    authorizationEndpointUrl: https://mcp.cloudflare.com/authorize
    tokenEndpointUrl: https://mcp.cloudflare.com/token
    registrationEndpointUrl: https://mcp.cloudflare.com/register
    usePkce: true

Third, create a CredentialBinding that ties a specific workload and user together. This tells Riptides which agent should have its outgoing requests injected with the credential, and on behalf of which user:

apiVersion: core.riptides.io/v1alpha1
kind: CredentialBinding
metadata:
  name: claude-cloudflare-mcp-alice
spec:
  credentialSource: cloudflare-mcp
  workloadID: claude
  humanID: spiffe://acme.corp/employee/acme.corp/alice
  propagation:
    injection:
      selectors:
        - app: cloudflare-mcp

The humanID field is a SPIFFE ID representing the user who authenticated during the flow. It maps directly to the act claim in the Riptides JWT, while workloadID maps to the sub claim. Together they form the key the kernel uses to look up and inject the right credential. The binding ensures that only requests from the claude workload, acting on behalf of that specific user, will have the real credential injected. For larger deployments, bindings can be managed by user group or by arbitrary claims from the user’s ID token rather than per individual user.

The intermediate authorization flow, the JWT issuance, and the credential injection all happen automatically once these resources are in place.

Demo

The video below shows the complete double auth flow against Cloudflare MCP. Claude initiates the OAuth flow, the user logs in to Riptides, and is then redirected to Cloudflare to authorize Riptides as a client toward the MCP server. After both authentications complete, Claude is connected.

At the end we inspect Claude’s credentials file to verify the outcome:

cat ~/.claude/.credentials.json | jq

The file contains the Riptides JWT, not a Cloudflare access token.

Conclusion

Remote MCP servers introduced a new class of credential into the agentic workload stack: OAuth access tokens representing delegated user permissions. The standard handling pattern (store the token, attach it to requests) exposes sensitive credentials in agent memory and creates a target for attackers.

By acting as an intermediate authorization server and brokering the authentication on the agent’s behalf, Riptides removes the real credential from the agent entirely. The agent follows a fully standard OAuth2 flow. The kernel handles the rest.

This extends the same secretless model we apply to cloud provider credentials to the OAuth world: workloads authenticate, participate in authorization, and make authenticated requests, without ever possessing the tokens that grant access. The credential lives in the kernel, bound to the workload that earned it, and disappears when it expires.

As agentic systems grow in autonomy and begin chaining tools across multiple remote MCP servers, this property becomes critical. An agent that can be prompted into exfiltrating its own credentials cannot exfiltrate credentials it does not have.


If you enjoyed this post, follow us on LinkedIn and X for more updates. If you'd like to see Riptides in action, get in touch with us for a demo.
oauth credentials mcp agentic

Ready to secure your
workloads?

Kernel-level identity and enforcement. No code changes. Deploy in minutes.