Post

MCP and OAuth 2.0: A Match Made in Heaven

Exploring how MCP uses modern OAuth2 workflows and adjacent RFCs to enhance security and authorization.

MCP and OAuth 2.0: A Match Made in Heaven

I had the opportunity to work with and implement various OAuth workflows a few years ago. While reviewing the MCP specification, I noticed they went all-in on OAuth, even making use of all some fancy new RFCs. ‘New’ is a bit of a stretch, we’re talking about 2015-2022. But by internet standards, those RFCs are still babies. For comparison, SSH was created in 1995, SNMP in 1982, and FTP in 1971!

What’s MCP, Why is it a big deal?

MCP (Model Context Protocol) is a standard that LLMs can use to interface with external tools. If a server/API exposes an MCP interface, any LLM can just connect to it and make use of it without any custom integrations. It’s basically plug-and-play for LLM actions. If I can connect my LLM (Claude or ChatGPT) with a GitHub MCP server, then I can ask my LLM to interact with my account (list projects, issues, create them, etc.). This is a big deal. This basically allows AI agents (the term is irksome, I agree, but let’s indulge) to discover tools and capabilities and use them—it actually brings agency and action-capability to AIs in a standardized way.

Here is a deep dive of yours truly into MCP.

MCP is not the be-all and end-all. It scales poorly: as the number of tools available to the LLM grows, the context fills up and things start breaking.

But anyway, things will keep improving; there is no wall (or so we’ve been told), and we should dismiss any current limitations as soon-to-be old memories.

What does it have to do with OAuth?

So these MCP servers can be local (STDIO or standard IO) with fewer security risks, or they can be remote (HTTP), potentially traversing the dangerous internet, so security is paramount. HTTPS is obviously the bare minimum, but we also need a way to authenticate users and validate their permissions. Ideally, we have some authorization server that authenticates our potential users, makes sure they have access to the MCP server, and that they are willing to authorize the LLM (MCP client) to act on their behalf with the MCP server. Well, darn, that’s exactly what OAuth does.

OAuth

There are 3 key participants in OAuth:

  • The Authorization Server (AS), e.g., Google, Facebook, Apple, Okta, etc.
  • The Resource Server (RS), e.g., Calendar, Drive, or any app that has access to your data.
  • The Client App: A third-party app, unrelated to the AS and RS, that wants access to your data.

The classic example of OAuth is authorizing a client app to access one of your resources on the RS using a token generated by the AS once you (the user) grant permission.

Here’s how it works: The client app redirects you to the AS (Google, Apple, your company’s AD, or any server that manages user identities and authorizations), and you’re prompted to approve access for the client app:

oauth approve

Once you approve, the AS generates a ‘code’ that the client app (using a secret provided by the AS, called the client secret) exchanges for a token. How? When the client app is registered with the AS, it provides a redirect URL that the AS will use to send the user back after approval. The AS includes the ‘code’ in the redirect URL, and the client app (on the backend) exchanges that code for a token. The token is then used to interact with the RS. The RS must verify the token to confirm that the AS/user indeed authorized the app to access those resources.

The sequence diagram looks like this:

alt text

It’s a lot, I agree, but OAuth 2.0 (or OAuth 2.1, which is a cleanup and consolidation of OAuth 2.0 and various related RFCs) has been tried and tested, and it works!

OAuth for MCP

In the case of MCP, the AS is the AS. The RS is the MCP server. The client is the MCP client/LLM.

So the RS/MCP server wants to make sure that the user authorized the client (LLM/MCP client) to access his resources.

The MCP server could be Github, Slack, Jira or any MCP-aware service. The user is redirected to AS (Cognito, Okta, or Auth0) to approve that the LLM (MCP/Oauth client) can access those otherwise private resources.

Used standards

The MCP specification makes use of the following Oauth related RFCs.

  • Authorization Server Metadata (RFC8414)
  • Dynamic Client Registration Protocol (RFC7591)
  • Protected Resource Metadata (RFC9728)
  • Resource Indicator (RFC8707)

Traditionally, when a client wants to access a (protected) resource, it needs to be manually configured with all the details about the Authorization Server (AS), such as the URLs for authorization, token exchange, revocation, and so on. This setup is often hardcoded into the client’s code.

But in transient environments, like with MCP, where end users might interact with multiple servers, this manual configuration becomes impractical. Preconfiguring a separate client for each MCP server just doesn’t scale and non technical users are not expected to know about ‘tokens’ and ‘client secrets’.

To solve this, we rely on Authorization Metadata Discovery: instead of hardcoding AS details, ASes expose their configuration in a standard format that clients can fetch dynamically. This allows clients to automatically discover relevant endpoints and capabilities of the AS without prior manual setup.

Building on that, RFC 9728 defines a way for RS (MCP server) to tell clients which AS they rely on. When a client makes a request, the resource server responds with a WWW-Authenticate header that includes a URL to the AS’s metadata. The client can follow that URL, fetch the metadata, and complete the necessary steps to obtain tokens—all without any hardcoded knowledge of the AS.

To sum up:

  • Protected Resource Metadata tells the client where the AS is
  • Authorization Server Metadata allows the client to get all AS info and configure endpoints on the fly.
  • Dynamic Client Registration Protocol allows the client to register with the server dynamically (no prior knoweldge of the AS is needed)

We can have multiple resource servers (RS) in OAuth 2.0, leading to ambiguity and potential risks (one token intended for RS1 but used with RS2), you allow the LLM to access your calendar but it uses the token interact with financial data.

RFC 8707 introduces a resource field to specify the target RS. This ensures access is granted according to the principle of Least Privilege, limiting the scope to only the requested resource.

The following sequence diagram from the spec provides a nice visual:

alt text

The MCP client and server do not require any prior integration; as long as they both follow the RFCs and standards, users can safely and automagically grant their LLMs access to their MCP-enabled resources.

Let’s look at some requests examples.

RFC 9728: Protected Resource Metadata

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Attempt to access the resource server
GET / HTTP/1.1
Host: mcp.example.com

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"

# Discover authorization server metadata
GET /.well-known/oauth-protected-resource
Host: mcp.example.com

HTTP/1.1 200 OK
Content-Type: application/json

{
  "authorization_servers": ["https://auth.example.com/"]
}

# Fetch authorization server configuration
GET /.well-known/oauth-authorization-server
Host: auth.example.com

HTTP/1.1 200 OK
Content-Type: application/json

{
  "issuer": "https://auth.example.com/",
  "authorization_endpoint": "https://auth.example.com/oauth/authorize",
  "token_endpoint": "https://auth.example.com/oauth/token"
}

RFC 8414: Authorization Server Metadata

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET https://auth.example.com/.well-known/oauth-authorization-server

{
  "issuer": "https://server.example.com",
  "authorization_endpoint": "https://server.example.com/authorize",
  "token_endpoint": "https://server.example.com/token",
  "jwks_uri": "https://server.example.com/jwks.json",
  "response_types_supported": ["code", "token"],
  "grant_types_supported": ["authorization_code", "implicit"],
  "revocation_endpoint": "https://server.example.com/revoke",
  "introspection_endpoint": "https://server.example.com/introspect",
  "code_challenge_methods_supported": ["S256"],
  "registration_endpoint": ""
}

RFC 7591 Dynamic Client registration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST /oauth/register HTTP/1.1
Host: auth.example.com
Content-Type: application/json
{
"client_name": "Claude",
"logo_uri": "https://claude.ai/logo.png",
"redirect_uris": ["https://auth.example.com/redirect"]
...
} 


HTTP/1.1 201 Created
Content-Type: application/json
{
"client_id": "ad2669221ba94de0ee0",
"client_secret": "6a58a307937e98c459be3bfe8e19af3a",
}

Authorize and Token Exchange

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# First authorize redirect
GET /authorize?
   response_type=code&
   client_id=client123&
   redirect_uri=https://client.example.com/cb&
   scope=read&
   resource=https://api.example.com/

# code exchange for token
POST /token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=SplxlOBeZQQYbYS6WxSbIA&
redirect_uri=https://client.example.com/cb&
resource=https://api.example.com/

Auth0 and MCP Server Example Setup

For a more realistic setup, you can check my Auth0 with the MCP Python SDK integration.

Conclusion

Anyway, hope you learned a thing or two. If you want to get in touch, you can find me on Twitter/X at @moncef_abboud.

This post is licensed under CC BY 4.0 by the author.