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
MCP authentication is evolving from a less secure, API key-based system to a more secure OAuth 2.1 standard.
The traditional method was a client-server model where the MCP server handled both resource and authorization, creating a monolithic and non-scalable system.
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.
Implementing the OAuth flow involves using an authorization server, which can be either self-embedded or delegated to a provider.
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!
credits - MCP Specs
So, the flow goes as:
• MCP Client sends a request to the MCP Server without any credentials or authentication. • MCP Client receives the unauthorized response and retrieves its stored API key from configuration. • MCP Server receives the request and verifies if the API key is valid and has permission for this operation. • MCP Server sends back a successful response with the result or status information. • MCP Server continues to verify the API key on each request to its protected resources to ensure security and proper authorization.MCP Auth Flow - Generic
• MCP Server checks the request and responds with 401 Unauthorized because no credentials were provided.
• MCP Client sends the same request again but this time includes the API key in the Authorization Header.
• If the API key is valid, MCP Server processes the request and retrieves or updates the requested data.
• 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.
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:
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 Resource Server returns 401 Unauthorized MCP Client fetches the resource metadata and determines the appropriate Authorization Server. After metadata is fetched: MCP Client opens the user's browser and redirects to the, 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 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. MCP OAuth 2.1 Flow
with a www-authenticate header
pointing to metadata URL.
- the client can register itself dynamically at the Authorization Server,
- This is done using a POST /register
.
Authorization Server's authorization endpoint with OAuth params
(including code_challenge and resource).
Authorization Server's token endpoint to exchange for an access token.
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:
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)
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.
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
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:
In essence It contains everything needed to create a login system where users must authenticate before accessing protected MCP resources & tools. 📂simple-auth-clientFolder Overview
📂 simple auth
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.
main.py
: Orchestrate everything - OAuth flow management, token storage, and interactive tool calling sessions.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
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
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
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!
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 Now, the user clicks the provided link, and the process proceeds to the Authentication Phase. 2. Authentication Phase However, the client is not authorized, so the process moves to the Token Exchange & Validation Phase. 3. Token Exchange & Validation Phase Flow then moves to the Validation Phase: 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 It calls the The Resource Server checks the token through the Detailed Flow
.well-known/oauth
endpoint to discover the Authorization Server's details and present the OAuth link to the user.
SimpleAuthClient
in main.py
automatically finds where the Authorization Server lives and what OAuth features it supports.
401 Unauthorized error
. This is intentionally done to showcase the failure case.
SimpleAuthProvide**r**
class in auth_server.py
handled this by checking the credentials against what the user typed.
SimpleOAuthProvider
creates an authorization code, and the client receives it from the Authorization Server.
CallbackServer
in main.py
waits for the OAuth redirect while the user logs in through their browser.
InMemoryTokenStorage
class in main.py
is used to keep the retrieved token safe in the client's memory./token
endpoint at auth_server
powered by brain simple_auth_provider
.
IntrospectionTokenVerifier
class in token_verifier.py
does this job by calling the Authorization Server and asking, "Is this token still good?".
get_time
tool.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
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:
- Use established providers: Services like Composio handle security updates and compliance.
- Implement proper scoping: Only grant the minimum permissions needed.
- Monitor access: Keep track of what your AI agents are doing.
- Regular token rotation: Ensure tokens expire and refresh properly.
- 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)