MCP Authorization
The official new revised version supports OAuth2 authentication method, let’s take a look together.
First, the protocol requirements:
- STDIO not supported, STDIO only supports passing through env
- SSE/Streamable HTTP should support it (non-mandatory)
MCP follows the OAuth2 standard, mainly involving:
- OAuth 2.1 IETF DRAFT
- OAuth 2.0 Authorization Server Metadata (RFC8414)
- OAuth 2.0 Dynamic Client Registration Protocol (RFC7591)
Among them, OAuth2.1 is still in the draft stage and has not yet become an official RFC standard. Here’s a simple summary of OAuth-related RFCs:
Standard/Draft | Type | Status | Core? | Introduction |
---|---|---|---|---|
RFC 6749 | OAuth 2.0 Framework | ✅ Published | ✅ Yes | Defines authorization flows (authorization code, implicit, password, client credentials) and four major roles (client, resource owner, authorization server, resource server) |
RFC 6750 | Bearer Token Usage | ✅ Published | ✅ Yes | Describes how to securely use access tokens (Bearer Token) in HTTP |
RFC 7591 | Dynamic Client Registration | ✅ Published | ❌ Extension | Allows clients to dynamically register to the authorization server via API |
RFC 8414 | Authorization Server Metadata | ✅ Published | ❌ Extension | Provides a .well-known endpoint for exposing server configuration information, supporting auto-discovery |
draft-ietf-oauth-v2-1 | OAuth 2.1 Draft | ⏳ In Draft | ✅ Proposed Core | Summarizes and updates OAuth 2.0 core content, merges and replaces RFC 6749 and RFC 6750, eliminates insecure authorization methods (such as implicit authorization) and enforces PKCE |
OAuth 2.0 & 2.1
OAuth 2.0 - RFC 6749 & RFC 6750
Let’s briefly go through the OAuth content in RFC 6749, which covers the definition of OAuth2.0, focusing on the important parts:
- Register Client, obtain ClientID and ClientSecret
-
Configure on the Client, when authorization is required, the Client (usually the browser) redirects to /authorize to request user authorization
GET /authorize? response_type=code& client_id=abc123& redirect_uri=https://client.com/callback& scope=read write& state=xyz123
-
The authorization server redirects back with the code
HTTP/1.1 302 Found Location: https://client.com/callback?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz123
-
Client or Server uses the authorization code to exchange for AccessToken and RefreshToken
POST /token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code& code=SplxlOBeZQQYbYS6WxSbIA& redirect_uri=https%3A%2F%2Fclient.com%2Fcallback& client_id=abc123& client_secret=secret456
-
Using the token, you can request the corresponding resources (resource scope determined by the scope)
{ "access_token": "2YotnFZFEjr1zCsicMWpAA", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA" }
RFC 6750 basically supplements OAuth 2.0, mainly specifying that AccessToken is represented as a Bearer Token (Authorization: Bearer
OAuth 2.0 Authorization Server Metadata - RFC 8414
The /authorize and /token endpoints mentioned earlier are fixed or pre-agreed configurations, which may not be convenient in some scenarios. Therefore, this RFC provides an automatic discovery mechanism, somewhat similar to openid-configuration in OIDC.
GET https://auth.example.com/.well-known/oauth-authorization-server
# Multi-tenant
GET https://auth.example.com/.well-known/oauth-authorization-server?issuer=https://issuer.example.com
This adds an endpoint (the Discovery Endpoint, .well-known/oauth-authorization-server) for discovering related configurations. The configuration (Metadata Document, authorization server metadata in JSON format) might look like:
{
"issuer": "https://mcp-github-oauth.ifuryst.workers.dev",
"authorization_endpoint": "https://mcp-github-oauth.ifuryst.workers.dev/authorize",
"token_endpoint": "https://mcp-github-oauth.ifuryst.workers.dev/token",
"registration_endpoint": "https://mcp-github-oauth.ifuryst.workers.dev/register",
"response_types_supported": [
"code"
],
"response_modes_supported": [
"query"
],
"grant_types_supported": [
"authorization_code",
"refresh_token"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"none"
],
"revocation_endpoint": "https://mcp-github-oauth.ifuryst.workers.dev/token",
"code_challenge_methods_supported": [
"plain",
"S256"
]
}
Field Name | Required | Type | Description |
---|---|---|---|
issuer | ✅ Yes | string | Unique identifier of the authorization server (URL), must be https, no parameters or fragments |
authorization_endpoint | ✅ Yes (unless not supporting auth code flow) | string | OAuth authorization endpoint address for obtaining user authorization |
token_endpoint | ✅ Yes (unless only supporting implicit) | string | Token endpoint where clients obtain access tokens |
jwks_uri | ⛔ Optional | string | JWK Set address containing public keys for clients to verify JWT signatures |
registration_endpoint | ⛔ Optional | string | Endpoint for registering clients when dynamic client registration is supported |
scopes_supported | ⛔ Recommended | array | List of supported scopes |
response_types_supported | ✅ Yes | array | Supported response types, such as code, token |
response_modes_supported | ⛔ Optional | array | Supported response modes, such as query, fragment, form_post |
grant_types_supported | ⛔ Optional | array | Supported grant types, such as authorization_code, client_credentials |
token_endpoint_auth_methods_supported | ⛔ Optional | array | Client authentication methods supported by the token endpoint, such as client_secret_basic |
token_endpoint_auth_signing_alg_values_supported | ⛔ Optional | array | Signature algorithms supported when using JWT authentication at token endpoint, such as RS256 |
service_documentation | ⛔ Optional | string | Developer documentation address |
ui_locales_supported | ⛔ Optional | array | List of languages supported by UI (e.g., zh-CN) |
op_policy_uri | ⛔ Optional | string | URL for the authorization server’s policy on client data usage |
op_tos_uri | ⛔ Optional | string | Terms of service URL |
revocation_endpoint | ⛔ Optional | string | Token revocation endpoint (see RFC 7009) |
revocation_endpoint_auth_methods_supported | ⛔ Optional | array | Authentication methods supported by the revocation endpoint |
revocation_endpoint_auth_signing_alg_values_supported | ⛔ Optional | array | JWT signature algorithms supported by the revocation endpoint |
introspection_endpoint | ⛔ Optional | string | Token status check endpoint (see RFC 7662) |
introspection_endpoint_auth_methods_supported | ⛔ Optional | array | Authentication methods supported by the introspection endpoint |
introspection_endpoint_auth_signing_alg_values_supported | ⛔ Optional | array | JWT signature algorithms supported by the introspection endpoint |
code_challenge_methods_supported | ⛔ Optional | array | Supported PKCE code_challenge_method (e.g., S256) |
Additionally, service providers can add custom fields.
OAuth 2.0 Dynamic Client Registration Protocol - RFC 7591
The previous steps for registering to obtain ClientID were manual. This RFC essentially allows clients to automatically register themselves to get a ClientID and ClientSecret without requiring pre-registration by humans.
Registration example:
POST /register HTTP/1.1
Host: server.example.com
Content-Type: application/json
{
"client_name": "AwesomeApp",
"redirect_uris": [
"https://awesome.example.com/oauth/callback"
],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "read write",
"token_endpoint_auth_method": "client_secret_basic"
}
Possible response:
{
"client_id": "s6BhdRkqt3",
"client_secret": "7Fjfp0ZBr1KtDRbnfVdmIw",
"registration_access_token": "access-token-123",
"registration_client_uri": "https://server.example.com/register/s6BhdRkqt3",
"client_id_issued_at": 1599389946,
"client_secret_expires_at": 0
}
OAuth 2.1
This revises OAuth 2.0 rather than completely rewriting it. The main differences are:
- Implicit and Resource Owner Password Credentials methods were removed due to security concerns
- All clients are required to use PKCE
- Refresh token rotation is recommended (the old one is discarded when refreshed)
I think the most important obvious difference is the mandatory use of PKCE (Proof Key for Code Exchange), a mechanism designed to prevent authorization code interception and replay, which basically works as follows:
- The client randomly generates a string, which is the code_verifier
- After SHA256 hashing and Base64 encoding the code_verifier, another random string is obtained, which is the code_challenge
- The client can send the code_challenge and the corresponding hash algorithm code_challenge_method=S256 when making a request
- After the user agrees to authorize, the server records the code_challenge
- When the client exchanges the authorization_code for a token, it must include the original code_verifier
- The authorization service will hash the code_verifier and compare it to confirm whether to accept the request
Let’s look at the code:
import secrets
import hashlib
import base64
def generate_code_verifier(length=64):
# PKCE specification recommends length between 43-128 characters
return base64.urlsafe_b64encode(secrets.token_bytes(length)).rstrip(b'=').decode('utf-8')
def generate_code_challenge(code_verifier):
code_verifier_bytes = code_verifier.encode('utf-8')
sha256_digest = hashlib.sha256(code_verifier_bytes).digest()
code_challenge = base64.urlsafe_b64encode(sha256_digest).rstrip(b'=').decode('utf-8')
return code_challenge
# Example
code_verifier = generate_code_verifier()
code_challenge = generate_code_challenge(code_verifier)
print("code_verifier:", code_verifier)
print("code_challenge:", code_challenge)
# code_verifier: 92Foogx4d9Q5cbDbmLrz7eCHfAxX06q-6FHhmyKQ0OMcGpRbu6CWzknxCUSuvJ6b5-D_dIaJB5mHfAIfk_Qu1A
# code_challenge: GFc8vy-W93jTehp7I3Fvzma2DH5JNjnRAoktZuHtywA
So the similar request is:
# Initiate authorization request
GET /authorize?
response_type=code&
client_id=abc123&
redirect_uri=https://client.example.com/cb&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256&
state=xyz
# 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&
client_id=abc123&
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Practical Examples
GitHub OAuth APP Configuration




Configure according to the screenshots above, and finally obtain the corresponding Client ID and Client Secret.
CF Worker Deployment
Refer to https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-github-oauth
# 1. Initialize the project, yes for the first two steps, no for the last one because we haven't configured and can't deploy yet
npm create cloudflare@latest -- mcp-github-oauth --template=cloudflare/ai/demos/remote-mcp-github-oauth
# 2. Install wrangler
npm install -g wrangler
# 3. Configure client id and client secret
cd mcp-github-oauth
wrangler secret put GITHUB_CLIENT_ID
# Enter GitHub Client ID, then y
wrangler secret put GITHUB_CLIENT_SECRET
# Enter GitHub Client Secret
wrangler secret put COOKIE_ENCRYPTION_KEY
# Enter a random string, you can use openssl rand -hex 32
# 4. Set up KV namespace
wrangler kv:namespace create "OAUTH_KV"
# This will generate a corresponding id, copy and write to the wrangler.jsonrc file
"kv_namespaces": [
{
"binding": "OAUTH_KV",
"id": "abc123"
}
],
# 5. Deploy to Worker, this will jump to browser login and similar operations, finally select the user to upload
npm run deploy
Screenshots of the related operations are as follows:




Testing
Currently, there are few clients that support MCP authentication, and cursor has no plans to support it at the moment. We’ll use the official inspector for testing:
npx @modelcontextprotocol/inspector@latest
Connect via SSE, such as https://mcp-github-oauth.ifuryst.workers.dev/sse
The general flow is: after connecting, a 401 is returned because there is no authentication authorization. At this point, the MCP Client will discover authentication information based on the Server Metadata Discovery exposed by MCP Servers (at https://mcp-github-oauth.ifuryst.workers.dev/.well-known/oauth-authorization-server) and redirect to the corresponding address for authentication. Here, it will first go to the service page deployed on CF Worker, and after clicking confirm, it will jump to GitHub for actual authentication, and finally jump back to the MCP Client’s callback interface, usually /oauth/callback, such as http://127.0.0.1:6274/oauth/callback for Inspector.





The entire flow in the browser is relatively smooth. If it’s a client-side MCP Client, it typically redirects to the browser for login or opens an external webpage within the application for login. This involves the application itself also needing to listen, as it needs to handle the callback.
Conclusion
MCP is a young protocol, proposed just half a year ago, and the authentication scheme was only included in the March revision. There is actually some controversy here, with some people believing this is not best practice. We can see the discussion here. That is, now the MCP Server is more often treated as an OAuth authorization server, which is a burden for MCP Server providers. Most MCP Servers tend to be lightweight or microservice-oriented, and requiring them to integrate the corresponding authentication is undoubtedly a huge effort.
Based on this, MCP Gateway is currently developing a client-side authentication system, allowing MCP Gateway to adapt to more scenarios, and various services can also connect to MCP Gateway to quickly adapt to authentication scenarios.
If you’re interested in my open-source project, feel free to use, provide feedback, and participate in any contributions:
https://github.com/mcp-ecosystem/mcp-gateway
Enjoy Reading This Article?
Here are some more articles you might like to read next: