The man-in-the-middle pattern for MCP server OAuth | Tigris Object Storage

TL;DR: your MCP Server needs to be an OIDC provider, a man-in-the-middle pattern. If you wrote your MCP Server in JavaScript, use the oidc-provider library to make your life easier. Just add your key-value storage layer of choice to manage state. We used Tigris as our kv store.

Recently we announced the hosted version of our Model Context Protocol (MCP) server so you can vibe code with Tigris to your heart’s content. One of the tricker parts of building it was implementing OAuth 2 with Auth0– nothing seemed to work out of the box, and every tutorial we could find was outdated just months after publish. We hope that by writing this, we give you the guide to implementing MCP OAuth that we wish we had when we set out on making our MCP server happen. Today, you’ll learn all the moving parts and constraints that go into making MCP Authentication work.

As a sneak preview into the madness we had to unveil, we ended up needing to implement a hybrid OpenID Connect (OIDC) server where our MCP server is a partial OIDC passthrough proxy to Auth0, but also issues its own tokens under the hood. Ready? Let’s begin.

MCP Authentication is easy, right?

In a typical web app, your backend is the OAuth client. The resource owner is the human user. You manually configure a client ID and secret, redirect URL, and register it with your identity provider of choice. But for an MCP server, the resource owner is still the human user, but they configure their MCP client (LLM, Cursor, etc) to make OAuth protected API calls on their behalf.

So your mental model is:

User
MCP
Client
MCP
Server
OAuth-protected
API

Instead of:

User
Your App
OAuth-protected
API

The OAuth flow is typically done in the browser with a redirect URL. But MCP servers aren’t in browser, so they have to spawn their own HTTP server to receive the right codes. Sometimes they can’t even do that, so they have the identity provider spit out a code that users have to paste into their MCP client. It’s a mess full of edge cases that have to be discovered and accounted for.

The common patterns we see in implementing authentication for MCP are:

We picked the last option, but we hit a couple snags that others seem to be hitting as well:

The problem: MCP’s torrent of dynamic OAuth clients

At Tigris, we use Auth0 and whenever a dynamic client registers with Auth0, that client gets its own entry in the list of authentication clients in the Auth0 panel. This means you quickly get overrun with duplicate entries for every AI agent that every one of your users uses, like this:

As you can see, this does not scale cleanly. Sure, the identity provider could do some work to show you which users are associated with those dynamic applications, but this just ends up becoming onerous in practice. So we needed to make some service that sits in the middle to hide this badness away.

The solution: make your MCP server a man-in-the-middle

To make most IdPs reasonably functional for MCP servers, the MCP server itself needs to act as an OpenID Connect (OIDC, or OAuth with opinions added) identity provider backed by your existing identity provider. In a regular web app, you wouldn’t have to deal with this.

This pattern of having your app act as an OIDC server is common, examples from Clerk, Cloudflare, and Simplescraper all have the MCP server acting as a man in the middle. I’d imagine that they also have found UX difficulties involved with dynamic client registration or are using identity providers that do not support it. Either way, this also does allow you to limit the capabilities of the MCP server from an upstream level, which can allow a much more clean separation of responsibilities.

Our sins laid bare: Making our MCP Server an OIDC client backed by a kv store

Our MCP server is a Node.js app written with Express using the Model Context Protocol SDK. In order to avoid overloading Auth0 with a flood of dynamically registered clients, we made our MCP server a dual OIDC provider and OIDC client. When you authenticate with our MCP server, it proxies some OAuth calls to Auth0 and then uses that to facilitate its own session minting logic.

Practically, this means that instead of having clients use the IdP hosted endpoints found in /.well-known/, like /authorize, /token, or /register, you implement those 6ish endpoints directly on your MCP server. Even then, you don’t actually have to implement them yourself if you use the oidc-provider package like we did:

import { Adapter, Provider } from 'oidc-provider';

export const createOAuthProvider = (

issuerUrl: string,

auth0Config: Auth0Config,

storageAdapter: Adapter,

): { provider: Provider; customRoutes: Router } => {

// Create OIDC Provider instance

const provider = new Provider(

issuerUrl,

createOidcProviderConfig(storageAdapter);

);
...

return { provider, customRoutes };
};

The oidc-provider package implements these endpoints for you.

Without a library in the middle, you need to create a few routes in your webapp. Keep in mind that the openid-configuration route RFC-2119-MUST be at the /.well-known/openid-configuration route otherwise clients will not be able to discover where to do OAuth exchanges. Here’s some example routes based on what I personally find reasonable:

But seriously, use the library. It will take care of the majority of the OAuth flow for you.

Adding a stateful backend

When you implement this OIDC man in the middle approach, this makes your MCP server stateful instead of stateless. When you create OIDC sessions, client credentials, and all of the other state tokens, you need to store them somewhere. This shift from statelessness to statefulness can dramatically complicate deployment of your MCP server depending on the Facts and Circumstances™ of your deployment stack.

In order to keep our state management simple, our implementation uses Tigris as its storage backend. This means that all of Tigris’
efficient handling of many small files and implicit global replication works to our advantage. (This does come at the cost of our MCP server being unavailable when Tigris is down, but if Tigris is down then you can’t really use the Tigris MCP server in the first place... and
we’re over 99.99% available anyway).

Here’s what we store in Tigris. It’s a bunch of JSON that could fit into any kv store (even Postgres!):

The library handles saving state to the storage backend, just add your own adapter. Adapters are custom. We use one for Tigris (reach out if you’d like to use it!).

OAuth turtles all the way down

Another thing to keep in mind is that doing this means you need to add yet another level of indirection to your authentication flow. Normally the OAuth flow for a Web App that uses GitHub for authentication looks like this:

GitHubWeb AppClientGitHubWeb AppClientAuthorize me pleaseAsk GitHub to confirmAuthorize me pleaseAuthorized, please confirm with Web AppGitHub asked me to confirm with youSounds good, welcome back Xe!

In contrast, our authentication flow with the MCP Server and an Auth0 user backed by GitHub looks a bit more like this:

GitHubAuth0MCP ServerClientGitHubAuth0MCP ServerClientAuthorize me pleaseAsk Auth0 to confirmAuthorize me pleaseAsk GitHub to confirmAuthorize me pleaseAuthorized, please confirm with Auth0GitHub asked me to confirm with youUse the access code to get a tokenHere's a tokenAuthorized, please confirm with MCP ServerAuth0 asked me to confirm with youUse the access code to get a tokenHere's a tokenHere's your access token

More complicated, certainly, but most of this is handled ambiently by authentication providers and library code. In most cases, you’ll only need to implement setting up the Provider itself. In addition to the Adapter.

Client Registration

In order for any OAuth or OIDC logic to happen, the Client needs to register as an OAuth application with the MCP server. To make it happen, the Client makes a new application using the RFC7591 OAuth Dynamic Client Registration Protocol:

TigrisMCP ServerClientTigrisMCP ServerClientDiscover OIDC metadataOIDC metadata responseRegister client at /registerSave client detailsReturn client credentialsStart OAuth flowReturn interaction redirect URL

When registering with the server, the MCP client sends a request with information about the OAuth application kinda like this:

fetch("https://mcp.storage.dev/register", {

method: "POST",

body: JSON.stringify({

"client_name": "Glorbotron Coding Agent",

"redirect_uris": "glorbotron://callback",

"grant_types": ["authorization_code", "refresh_token"]

}),

}).then(...)

And then gets back an OAuth Client ID and Client Secret kinda like this:

{

"client_id": "40A156B3-9C02-4D7C-88D1-FBC9A225D0D6",

"client_secret": "A5492101-9FEC-4178-B4DA-AB1A3B3807FF",

"client_id_issued_at": "RFC 3339 timestamp"

}

For more information about how this part is implemented, read through the Client Registration Endpoint section of RFC 7591. This should be pretty straightforward, most of what you will need to do is figure out how to store data in a database somewhere and generate random secrets. My example secrets are UUIDv4s, but you can do basically anything you want and be fine.

Authorization with Auth0

From here the Client needs to create a new OAuth Interaction with the MCP server. This authorization request is what creates the login form on GitHub when you click the Login with GitHub button.

MCP ServerClientMCP ServerClientStart OAuth flow at /authorizeReturn interaction redirect URL to Auth0

This kicks off the three-way handshake between your client, our MCP Server, and Auth0. The Client gets a redirect to Auth0, which makes your browser open to the login page.

Authentication with Auth0

Now Auth0 takes over and does its own authentication logic. In many circumstances, it will also be a man-in-the-middle authentication service like our MCP server is. Your users could ultimately be authenticated by Google, Microsoft, Fly.io or GitHub. Auth0 is acting as the authentication aggregator in this equation.

Auth0TigrisMCP ServerClientAuth0TigrisMCP ServerClientUser authenticates with Auth0Redirect back to MCP serverRedirect with details from Auth0Verify details from redirectReturn access, id, refresh tokensSave user session detailsRedirect to start interaction

Once the MCP server finishes that handshake with Auth0, it saves the session data to Tigris and then kicks off its own Interaction.

The second Interaction

Now that the MCP server has details about your Auth0 session, it uses that to correlate it with its own authentication logic. That flow looks kinda like this:

TigrisMCP ServerClientTigrisMCP ServerClientStart interactionRequest client / session dataReturn client / session dataCreate OIDC grant tokenFinish Interaction, redirect to agent

Peace is restored to the kingdom

Everything is downhill from here. The code that the MCP Server sends back from a successful Interaction is used to mint a new access token. That access token is used by the Client every time it starts a new MCP session or makes a call. Peace gets restored to the kingdom and your vibes can flow out through your editor into the most holy of B2B SaaS applications.

TigrisMCP ServerClientTigrisMCP ServerClientExchange code for tokenRequest interaction dataReturn newly minted tokens

That’s pretty much all of the moving parts at play. In an ideal world, we wouldn’t have to ship about half of this. I’d really love it if Auth0 and other identity provider services were able to have dynamic clients split out into their own category and not clog up the list of statically configured clients, but we can live with this.

Issues we hit along the way

We hit a few other issues when implementing our MCP server, and most of them were adding server-side bodge logic to handle MCP Clients not requesting the scopes they needed or potentially having unpredictable redirect URLs. There was an issue we ran into with MCP Resources making it difficult to upload “large” files, but we can get more into that at a later date.

Most of the challenge here is with how quickly the MCP ecosystem is moving. There is a lot of specification velocity with three major versions released in the last year, but at the same time the ecosystem is moving very slowly because everything is contingent on clients and servers actually supporting new features. Clients won’t implement features unless servers support them and servers won’t implement features unless clients support them. It’s the classic IRCv3-style catch-22 where you can write a specification a decade ago and only ever see widespread adoption half a decade later. Hopefully we’ll move faster with MCP, and the speed will pay off.

Conclusion

That’s how we made our MCP server work! Try it out today and give your agents the object storage superpowers they deserve. Your agents will either thank you for the gift or tell you that you’re absolutely right, whatever the fates decree.

Oh, while we still have you, if you’re developing an MCP server, take a look at the MCP Inspector. It’s the equivalent of the browser inspector but for MCP servers. It lets you see everything an MCP server has to offer and lets you easily test your OAuth implementation as you work through that 4-way handshake we had to figure out. This was absolutely invaluable during development and without it we wouldn’t have been able to ship this at all.

Let us know if this guide on MCP OAuth was helpful to you or not, we have a few more things like this in the pipeline but need your feedback to know how to prioritize it.