DEV Community

Cover image for MCP OAuth 2.1 - A Complete Guide
Developer Harsh for Composio

Posted on • Originally published at composio.dev

MCP OAuth 2.1 - A Complete Guide

Blog Motivation

I have been in a lot of MCP discussions lately, and one common statement always resonated with me: MCP isn't secure - sharing an API key for authentication is bad.

And they might be right!

However, recently, MCP 2.1 introduced OAuth support (March 2025 revision), and it is gaining traction across the ecosystem.

So, I have dived into the MCP OAuth 2.1 to understand its nitty-gritty details, and in this blog, I will summaries what I have learnt over the past few weeks.

Let's get started!

TL;DR

  1. MCP authentication is evolving from a less secure, API key-based system to a more secure OAuth 2.1 standard.

  2. The traditional method was a client-server model where the MCP server handled both resource and authorization, creating a monolithic and non-scalable system.

  3. The new OAuth flow separates these roles, with an external authorization server handling the authentication logic, which makes the system more modular and secure. - a code implementation provided.

  4. Implementing the OAuth flow involves using an authorization server, which can be either self-embedded or delegated to a provider.

  5. Key safety measures for MCP authentication include using PKCE, standard metadata, dynamic client registration, and other best practices like regular token rotation and secure storage

Having TL;DR under the belt, let's start by understanding the need for new auth method


The Need for MCP OAuth 2.1

(Old MCP OAuth in Nutshell)

To understand the need for MCP Auth 2.1, one need to understand how earlier mcp authentication used to actually work!

So, We know that MCP works with a client–server setup.

The cool part is that MCP authentication uses this same setup to do the login process, since the client already has to talk to the server anyway. Here is how it works”

MCP Auth has two components

  • MCP Client - Usually Claude, Cursor, VSCode
  • MCP Server - acts as a Resource server to host. Server data and operation + perform authorization logic (perform auth and permission check internally)

Here is a diagram to help you understand the flow!

MCP Specs

credits - MCP Specs

So, the flow goes as:

MCP Auth Flow - Generic

• MCP Client sends a request to the MCP Server without any credentials or authentication.
• MCP Server checks the request and responds with 401 Unauthorized because no credentials were provided.

• MCP Client receives the unauthorized response and retrieves its stored API key from configuration.
• MCP Client sends the same request again but this time includes the API key in the Authorization Header.

• MCP Server receives the request and verifies if the API key is valid and has permission for this operation.
• If the API key is valid, MCP Server processes the request and retrieves or updates the requested data.

• MCP Server sends back a successful response with the result or status information.
• MCP Client receives the response and saves the API key in memory for future requests.
• For all future requests, MCP Client automatically includes the cached API key so no more challenges are needed.

• MCP Server continues to verify the API key on each request to its protected resources to ensure security and proper authorization.

I hope you can see the problem - the MCP server acts as both a Resource & Authorization Server, making it a monolithic, self-contained system that is non-scalable, non-secure, and non-modular.

To mitigate the issue, OAuth 2.1 was introduced recently!


A Brief Intro to OAuth & Its Variations

But what is OAuth? Is it something new?

No, it's not a brand-new thing and you've probably used it before. For example, when you see a "Login with Google" or "Signup with GitHub" button, that's OAuth in action.

OAuth or Open Authorization is a security protocol that allows website and application to access resources or data from another webapps on behalf of user, without sharing any personal & sensitive personal info (e.g. password). Here is how it works in short…

OAuth Refresher

Here is a quick rundown on OAuth concepts:

Actors

  • Resource Owner (user): The one who permits access to their data.
  • Client (AI agent): The MCP client that requests primitives (tools, prompts, resources) from the MCP server.

  • Resource Server (MCP server): The backend that validates access and serves the data to the MCP Client

  • Authorization Server: Authenticates users and issues tokens. It can be separate (OAuth Flow) or part of the MCP server itself (traditional method)

Credentials and flow mechanics

  • Authorization code: A short-lived token used in the authorization code flow. It represents the user's approval for data exchange, and gets exchanged for tokens

  • PKCE Proof Key for Code Exchange: An OAuth extension that protects public clients by binding the authorization code to the client. Required in OAuth 2.1

  • Access token: A short-lived credential often a JWT that proves permissions when calling APIs

Together, these pieces enable apps to securely perform tasks for users without requiring the app to access sensitive credentials directly.
To learn more, check out this amazing blog.

Over time OAuth have gone through 3 major changes:

  • OAuth 1.0 (2007) - 1st version, bit complex to use and implement with security issues.

  • OAuth 2.0 (2021) - 2nd version, flexible to use, fixed most of the problems of 1.0 and used by most apps today - “Login with X”.

  • OAuth 2.1 (2025) - 2.1 version, more cleanup & secured version of 2.0, makes optional part in ver. 2.0 necessary to implement by developers.

As latest one came out in March 2025 called OAuth 2.1, and for MCP it became a defacto.

Now let’s look at how OAuth 2.1 Flow works with MCP.


How MCP Authentication Works in the OAuth 2.1 Flow?

With OAuth, the flow diagram changes to:

image.png

Note that there is an addition of the Authorization Server, which handles all the authentication logic previously handled by the MCP Server itself.

Now, the MCP Server only act for validating the token & providing back the resources requested by the client, the rest remains the same.

Here is the enhanced flow for curious minds:

MCP OAuth 2.1 Flow
  • MCP Client makes a request to the MCP Resource Server without a token.
  • MCP Resource Server returns 401 Unauthorized
    with a www-authenticate header pointing to metadata URL.

  • MCP Client fetches the resource metadata and determines the appropriate Authorization Server.

  • After metadata is fetched:
    - the client can register itself dynamically at the Authorization Server,
    - This is done using a POST /register.

  • MCP Client opens the user's browser and redirects to the,
    Authorization Server's authorization endpoint with OAuth params
    (including code_challenge and resource).

  • Authorization Server authenticates the user and displays the consent screen with requested scopes.

  • User accepts or denies the request.

  • Upon acceptance, Authorization Server redirects back to the client with an authorization code.

  • MCP Client sends the authorization code and code_verifier to the
    Authorization Server's token endpoint to exchange for an access token.

  • Optionally, MCP Client stores the access token for future use.

  • MCP Client makes the request to the MCP Resource Server with the access token in the header.

  • Resource Server verifies the token and responds with the requested data.

In essence, auth servers are responsible for:

  • Issuing signed access tokens with embedded claims
  • Supporting OAuth 2.1 flows (e.g., Authorization Code with PKCE)
  • Presenting consent screens
  • Enforcing token lifetime, refresh logic, and revocation
  • And more

Now the real question is, who provides those Authorization servers?


Authorization Servers Basics

In the MCP ecosystem, there are two ways to implement an authorization server:

  1. Self-Embedded: In embedded authorization servers, the MCP server acts as the Identity Provider, handling all the work of the authorization server. - As discussed in the traditional approach. (previously available, now outdated)

  2. Delegated: In the delegated approach, the task is delegated to external authorization server providers, such as Auth0, which handle all tasks, from centralized login and consent to token issuance.

Self Embedded Auth Server
Self Embedded Auth Server

Delegated Auth Server
Delegated Auth Server

Here is a simple comparison:

Category Self / Embedded Delegated / OAuth Approach
OAuth role MCP server acts as Identity Provider (Auth Server) and Resource Server MCP server acts as Relaying Party
Token issuance MCP server issues its own tokens Auth server issues tokens
Auth responsibility Entire identity flow handled within MCP server Delegated to external auth server or auth service
Integration effort High; requires building and managing an OAuth provider Low; leverages existing external auth services and auth providers

Next let's implement an OAuth flow via code ourselves to understand the new norms better!


Implementing MCP OAuth 2.1 Flow From Scratch

(Servers OAuth MCP Server & Client)

This flow aims to show how a user can authorize the MCP client once, and then the client can safely call protected MCP tools on their behalf.

For this, we will run an authorization server, a resource server, and a client (VS Code for me) to see how one can implement the OAuth login, so that users can approve tools without sharing passwords / any sensitive credentials, as was the case with API API-based approach.

To start, head to the terminal and clone this repo.

git clone https://github.com/devloper-hs/oauth-demo.git
cd oauth-demo
Enter fullscreen mode Exit fullscreen mode

Once in the directory, you will see that the codebase consists of two folders: simple-auth (server) and simple-auth-client (client). ‘

You can check out overview of both:

Folder Overview 📂 simple auth
  • This folder contains the complete OAuth 2.0 backend server, which protects MCP tools with authentication.
  • It contains everything needed to create a login system where users must authenticate before accessing protected MCP resources, like:
    • auth_server.py - handles login and issues tokens.
    • server.py ****- protects MCP tools & resources.
    • simple_auth_provider.py - core OAuth logic.
    • token_verifier.py - validates access tokens.
    • Along with `pyproject.toml` for configuration.

In essence It contains everything needed to create a login system where users must authenticate before accessing protected MCP resources & tools.

📂simple-auth-client

  • This folder contains the frontend application, which provides users with an interface to log in and access protected MCP tools after authentication.
  • It contains:
    • `__init*__*.py` : Empty file for modularization
    • main.py : Orchestrate everything - OAuth flow management, token storage, and interactive tool calling sessions.
    • Along with pyproject.toml for configuration.

Yes, we need to run the client and server separately, so we will start them next.

In the 1st terminal, start the authorization server by navigating to simple-auth folder and starting the server at port 9000.

cd simple-auth
uv run mcp-simple-auth-as --port=9000
Enter fullscreen mode Exit fullscreen mode

Now open a second terminal and with the same simple-auth Folder: Start the resource server at port 8001 & point it to the Terminal 1 auth server. The MCP transport used here is HTTP as it's now a standard.

uv run mcp-simple-auth-rs --port=8001 --auth-server=http://localhost:9000 --transport=streamable-http
Enter fullscreen mode Exit fullscreen mode

Notice we have two different ports running two distinct components of the same MCPc at two other ports.

This allows for modularity and scalability, which were lacking in the previous MCP spec, as any part can be easily replaced with another without affecting the main functionality. - Next time, initiate the client.

In 3rd terminal navigate to simple-auth-client, start the mcp-simple-auth-client script and point it to the authorization server with type HTTP ****stream.

MCP_SERVER_PORT=8001 MCP_TRANSPORT_TYPE=streamable_http uv run mcp-simple-auth-client
Enter fullscreen mode Exit fullscreen mode

Once you are done and everything runs as expected, a webpage opens where you can log in. If successful, the client will authenticate, and you can then use the MCP server.

Here is a quick demo of doing so from the start!

In case you messed up, make sure to check out readme.md file of the repo!

Note: The initial code was forked from original MCP repo, but I tweaked it a lot. The core changes include:

  • addition of new resources (tools) in servers.py.
  • support for a resource consent in simple_auth_server.py & main.py.
  • complete revamp of the UI/UX screen, including the addition of templates.
  • Error fixes.

But how does everything tie up?

How does our OAuth work?

In short, the whole system works like a three-part team.

  • The Authorization Server (auth_server.py) is like a security guard who checks if the user knows the password.
  • The Resource Server (server.py) is like a treasure keeper who has the good stuff but always asks the security guard, "Is this person allowed?".
  • The Client (main.py) is like a user, trying to get the treasure by first proving to the security guard that the user belongs there.

For curious mind here is the detailed flow, rest can skip to next section!

Detailed Flow

Here is what’s happening behind the scenes:

Whenever the client is run, it waits for OAuth to complete and access the resource. The client initiates OAuth and involves the user in logging into the Authorization Server via a web browser. This is done in the following phases:

1. Discovery Phase

  • The client's attempt to access the protected resource triggers a 401 Unauthorized error.
  • This prompts the client to use the GET .well-known/oauth endpoint to discover the Authorization Server's details and present the OAuth link to the user.
  • Behind the scenes, the SimpleAuthClient in main.py automatically finds where the Authorization Server lives and what OAuth features it supports.

Now, the user clicks the provided link, and the process proceeds to the Authentication Phase.

2. Authentication Phase

  • The first login attempt fails due to an incorrect password, resulting in a 401 Unauthorized error. This is intentionally done to showcase the failure case.
  • Behind the scenes, the SimpleAuthProvide**r** class in auth_server.py handled this by checking the credentials against what the user typed.
  • On the 2nd attempt, after entering the right credentials, the  SimpleOAuthProvider creates an authorization code, and the client receives it from the Authorization Server.
  • In meantime the CallbackServer in main.py waits for the OAuth redirect while the user logs in through their browser.

However, the client is not authorized, so the process moves to the Token Exchange & Validation Phase.

3. Token Exchange & Validation Phase

  • Once the user logs in, the client trades its authorization code for an access token that it can use later.
  • The InMemoryTokenStorage class in main.py is used to keep the retrieved token safe in the client's memory.
  • Important: The terminal output confirms 200 OK responses when the token exchange happens in the /token endpoint at auth_server powered by brain simple_auth_provider .

Flow then moves to the Validation Phase:

  • Then an API call is made to POST `/introspect` which validates the access token.
  • The IntrospectionTokenVerifier class in token_verifier.py does this job by calling the Authorization Server and asking, "Is this token still good?".
  • This introspection process enables the Resource Server to verify the validity of tokens and grant access to previously protected resources.

Now that the user is logged in, the only remaining task is to fetch the resource in the Access Resource Phase.

4. Access Resource Phase

  • Finally, the client uses this validated authentication to call a protected resource successfully. get_time tool.
  • It calls the get_time function that lives in server.py. If you check the function, it is decorated with @app.tool() making it a protected MCP tool.

    @app.tool()
        async def get_time() -> dict[str, Any]:
    
            """
            Get the current server time.
            This tool protects the system information by OAuth authentication. 
            User must be authenticated to access it.
            """
            ...
        return app
    
  • The Resource Server checks the token through the verify_token The method confirms its validity and, if valid, returns the current server time along with the time zone information.




Hope this clarifies how the OAuth server and client can be implemented and used. In case you want to learn more, and want custom integration, you can check it at docs📚.

However, it's of no use if someone can gain access to the server and access the data / spoof the authentication flow, so MCP 2.1 introduces a few standards that must be followed.


MCP Auth Safety Guidelines (New Benifits)

Most of the time, an oAuth provider handles all the safety guidelines; however, if you are creating a custom oAuth flow, you need to ensure you meet most of the specs mentioned in MCP 2.1 Spec Doc

These are considered best practices according to the ecosystem. A few important ones are:

  • PKCE: All MCP auth flows must follow the OAuth 2.1 standard, which requires using PKCE to protect authorization code exchanges.
  • Authorization Server Metadata: MCP servers should share standard metadata (RFC 8414) so clients can easily find supported endpoints and features.
  • Dynamic Client Registration: MCP servers should enable clients to register automatically, making onboarding faster and simpler.

Apart from this, some best practices can be followed by anyone building for production:

  1. Use established providers: Services like Composio handle security updates and compliance.
  2. Implement proper scoping: Only grant the minimum permissions needed.
  3. Monitor access: Keep track of what your AI agents are doing.
  4. Regular token rotation: Ensure tokens expire and refresh properly.
  5. Secure storage: Never hardcode credentials or tokens in your applications.

These fundamental measures ensure that MCP authentication is robust, discoverable, and scalable, promoting interoperability across diverse clients and authorization servers.

But how are oAuth flow helpful in real world?


OAuth Benefits – A Real-World Use Case

Let’s say you are using an AI coding agent that connects to my GitHub account.

Without OAuth

You have to create a personal access token (like a secret key) on GitHub and paste it into the agent.

The problem is that token might give the agent way too much power, like deleting repos, even if you only wanted it to read issues.

And what if the token ever leaked, anyone could use it until you deliberately go in and remove it.

With OAuth

The agent just sends me to GitHub’s login screen.

You see exactly what permissions it’s asking for, like *“read-only access to issues and pull requests.” * If you agree, GitHub gives the agent a temporary token.

Later, if you change your mind, just click “revoke” in my GitHub settings, and boom—the agent loses access right away.

So basically, OAuth makes things safer (short-lived tokens), clearer (you know what you are allowing), and easier to control (you can revoke anytime*). *

With this, we have come to the end of this short guide and let me close the post with final thoughts.


Final Thoughts

It’s interesting to see how fast the ecosystem is growing. OAuth is already turning into the standard way of doing things instead of just an optional add-on.

MCP used to rely on API keys for authentication, which, to be honest, wasn’t the most secure setup. People used to say “MCP isn’t secure,” and back then, they were right.

However, it is now shifting to OAuth 2.1, which feels like a significant upgrade in terms of security and scalability. With OAuth in place, that’s not really the case anymore.

Primarily, as MCP is adopted in remote/enterprise setups, delegated authentication (such as through Composio) is an efficient solution. It’s both safer and easier to work with.

If you want to check out more about MCP, here are some links I found useful:

Top comments (0)