-
-
Notifications
You must be signed in to change notification settings - Fork 204
Description
RFC: OAuth 2.1 Authorization Server for MCP
Phase: 4 — AI-Era Auth
Priority: P2 — High
Estimated Effort: High
Depends on: OIDC Provider (#514), M2M Auth (#509)
Problem Statement
The Model Context Protocol (MCP) requires an OAuth 2.1-compliant authorization server for tool-server authorization. MCP clients (AI agents, IDEs, chat interfaces) need to authenticate with MCP servers through standard OAuth flows. WorkOS, Keycloak 26.4, and Clerk all support MCP auth. This is the fastest-growing auth use case.
Current Architecture Context
/authorizeendpoint exists with PKCE support/oauth/tokensupports authorization_code and refresh_token grants/.well-known/openid-configurationexists (basic)- No
/.well-known/oauth-authorization-server(RFC 8414) - No Dynamic Client Registration (RFC 7591)
- No Resource Indicators (RFC 8707)
- No refresh token rotation
Proposed Solution
1. OAuth 2.1 Compliance
OAuth 2.1 (draft-ietf-oauth-v2-1) consolidates best practices:
Mandatory changes:
- PKCE required on ALL authorization code flows (S256 only, remove
plainmethod) - Remove implicit grant — currently
response_type=tokenis supported; deprecate and remove - Refresh token rotation — each refresh issues a new refresh token, old one invalidated
- Refresh token sender-constrained — bind to client_id, reject if different client presents it
- Exact redirect URI matching — no wildcard or partial matching
Implementation in existing handlers:
// In authorize.go — reject implicit grant:
if responseType == "token" {
return oauthError(c, "unsupported_response_type",
"Implicit grant (response_type=token) is not supported. Use authorization_code with PKCE.")
}
// In authorize.go — require PKCE:
if codeChallenge == "" || codeChallengeMethod != "S256" {
return oauthError(c, "invalid_request", "code_challenge with S256 method is required")
}
// In token.go — refresh token rotation:
case "refresh_token":
// Validate old refresh token
// Issue new access_token AND new refresh_token
// Invalidate old refresh token immediately
// If old refresh token is reused after rotation → revoke entire token family (breach detection)2. Authorization Server Metadata (RFC 8414)
New endpoint: GET /.well-known/oauth-authorization-server
{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/authorize",
"token_endpoint": "https://auth.example.com/oauth/token",
"registration_endpoint": "https://auth.example.com/oauth/register",
"revocation_endpoint": "https://auth.example.com/oauth/revoke",
"jwks_uri": "https://auth.example.com/.well-known/jwks.json",
"scopes_supported": ["openid", "profile", "email", "offline_access", "mcp:tool:*"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "client_credentials", "refresh_token"],
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "none"],
"code_challenge_methods_supported": ["S256"],
"revocation_endpoint_auth_methods_supported": ["client_secret_basic"],
"service_documentation": "https://docs.authorizer.dev",
"resource_indicators_supported": true
}MCP clients discover Authorizer's capabilities via this endpoint.
3. Dynamic Client Registration (RFC 7591)
MCP clients need to register programmatically — they can't go through a manual admin setup.
New endpoint: POST /oauth/register
// Request
{
"client_name": "Claude Desktop",
"redirect_uris": ["http://localhost:8765/callback"],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
"application_type": "native",
"scope": "openid profile mcp:tool:read_file mcp:tool:search"
}
// Response
{
"client_id": "dyn_a1b2c3d4...",
"client_name": "Claude Desktop",
"redirect_uris": ["http://localhost:8765/callback"],
"grant_types": ["authorization_code"],
"token_endpoint_auth_method": "none",
"registration_access_token": "rat_xyz...", // for subsequent client management
"registration_client_uri": "https://auth.example.com/oauth/register/dyn_a1b2c3d4...",
"client_id_issued_at": 1711800000
}Registration access token: Used to read/update/delete the client registration via GET/PUT/DELETE /oauth/register/:client_id.
Security controls:
--enable-dynamic-client-registration=false(off by default)--dcr-require-initial-access-token=true— require a pre-issued token to register (prevents open registration)- Rate limit on registration endpoint
4. Resource Indicators (RFC 8707)
Tokens are scoped to a specific MCP server (resource) to prevent token reuse across servers.
In authorization request:
GET /authorize?
client_id=dyn_abc&
response_type=code&
scope=mcp:tool:read_file&
resource=https://mcp-server.example.com& ← target MCP server
code_challenge=...&
code_challenge_method=S256
In token request:
POST /oauth/token
grant_type=authorization_code&
code=...&
resource=https://mcp-server.example.com
Token includes audience restriction:
{
"aud": "https://mcp-server.example.com",
"scope": "mcp:tool:read_file",
"sub": "user_123"
}MCP servers validate that aud matches their own URL before accepting the token.
5. Protected Resource Metadata
MCP servers publish metadata pointing to Authorizer as their authorization server:
Endpoint on MCP server side: GET /.well-known/oauth-protected-resource
{
"resource": "https://mcp-server.example.com",
"authorization_servers": ["https://auth.example.com"],
"scopes_supported": ["mcp:tool:read_file", "mcp:tool:execute_query", "mcp:tool:search"]
}Authorizer doesn't serve this endpoint (the MCP server does), but Authorizer needs to:
- Accept
resourceparameter in authorize/token requests - Set
audclaim to the resource URL - Validate that the resource URL is in an allowlist (optional)
6. Tool-Level Permission Scopes
Define MCP tool permissions as OAuth scopes:
mcp:tool:read_file → Read files from the MCP server
mcp:tool:write_file → Write/modify files
mcp:tool:execute_query → Execute database queries
mcp:tool:search → Search operations
mcp:tool:admin → Administrative operations
mcp:* → All MCP permissions
Scope registration: MCP server operators register their tool scopes via the admin API:
type Mutation {
_register_mcp_scopes(params: RegisterMCPScopesInput!): Response!
}
input RegisterMCPScopesInput {
resource_url: String! # MCP server URL
scopes: [MCPScopeInput!]!
}
input MCPScopeInput {
name: String! # e.g., "mcp:tool:read_file"
description: String! # Human-readable for consent screen
}Consent screen shows which tools the agent is requesting access to:
"Claude Desktop" wants to:
☑ Read files from your MCP server
☑ Search your data
☐ Execute database queries
☐ Write files
[Allow] [Deny]
MCP Auth Flow (End-to-End)
1. MCP Client discovers MCP Server's protected resource metadata
GET https://mcp-server.example.com/.well-known/oauth-protected-resource
→ learns authorization_server = https://auth.example.com
2. MCP Client discovers Authorizer's capabilities
GET https://auth.example.com/.well-known/oauth-authorization-server
→ learns endpoints, supported scopes, DCR endpoint
3. MCP Client registers dynamically (if not already registered)
POST https://auth.example.com/oauth/register
→ gets client_id
4. MCP Client initiates authorization
GET https://auth.example.com/authorize?
client_id=...&resource=https://mcp-server.example.com&scope=mcp:tool:read_file&...
→ user authenticates and consents
5. MCP Client exchanges code for token
POST https://auth.example.com/oauth/token
grant_type=authorization_code&code=...&resource=https://mcp-server.example.com
6. MCP Client uses token with MCP Server
MCP Server validates: signature (via JWKS), aud (matches own URL), scope, expiry
CLI Configuration Flags
--enable-dynamic-client-registration=false # Enable RFC 7591 DCR
--dcr-require-initial-access-token=true # Require token to register
--dcr-initial-access-token= # Pre-shared token for DCR
--enable-resource-indicators=true # Enable RFC 8707
--allowed-resources= # Allowlist of resource URLs (empty = any)
--enforce-pkce=true # Require PKCE on all auth code flows
--enable-refresh-token-rotation=true # Rotate refresh tokens
Migration Strategy
- Add
/.well-known/oauth-authorization-serverendpoint - Enforce PKCE on all authorization code flows (breaking: clients without PKCE will fail)
- Deprecate implicit grant (
response_type=token) with warning, then remove - Add refresh token rotation (existing refresh tokens continue to work but get rotated on next use)
- Add DCR endpoint (disabled by default)
- Add resource indicator support to authorize/token endpoints
Breaking changes: Implicit grant removal and mandatory PKCE. Provide migration period with warnings.
Testing Plan
- Test OAuth 2.1 compliance (PKCE mandatory, no implicit)
- Test refresh token rotation and breach detection
- Test DCR registration and client management
- Test resource indicator audience restriction
- Test MCP tool scope consent flow
- Test AS metadata endpoint
- Test backward compatibility with existing clients during migration