OAuth Flow Architecture#
Comprehensive documentation of the OAuth 2.1 flows implemented in the MCP OAuth Gateway, including dynamic client registration and PKCE.
OAuth 2.1 Implementation#
The gateway implements OAuth 2.1 with these key features:
Authorization Code Flow with PKCE (mandatory)
Dynamic Client Registration (RFC 7591)
Client Configuration Management (RFC 7592)
JWT access tokens with RS256 signing
GitHub OAuth for user authentication
Flow Overview#
Dynamic Client Registration (RFC 7591)#
Registration Request#
POST /register
Content-Type: application/json
{
"client_name": "My MCP Client",
"redirect_uris": ["http://localhost:8080/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "mcp:*",
"token_endpoint_auth_method": "none"
}
Registration Response#
HTTP/1.1 201 Created
Content-Type: application/json
{
"client_id": "client_7a3d5f2e",
"client_secret": "secret_9b4c6e8d",
"client_name": "My MCP Client",
"redirect_uris": ["http://localhost:8080/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "mcp:*",
"token_endpoint_auth_method": "none",
"registration_access_token": "reg-4f7d9c2a",
"registration_client_uri": "https://auth.example.com/register/client_7a3d5f2e",
"client_secret_expires_at": 1743638400
}
Registration Storage#
# Redis storage structure
oauth:client:client_7a3d5f2e = {
"client_id": "client_7a3d5f2e",
"client_secret": "secret_9b4c6e8d",
"client_name": "My MCP Client",
"redirect_uris": ["http://localhost:8080/callback"],
"registration_access_token": "reg-4f7d9c2a",
"created_at": "2024-01-01T00:00:00Z",
"expires_at": "2024-04-01T00:00:00Z"
}
JWT Token Structure#
Header#
{
"alg": "RS256",
"typ": "JWT",
"kid": "key-1"
}
Payload#
{
"iss": "https://auth.example.com",
"sub": "github|username",
"aud": "client_7a3d5f2e",
"exp": 1706745600,
"iat": 1704153600,
"jti": "token_3f8a9c2d",
"scope": "mcp:*",
"github_username": "username",
"github_id": "12345",
"github_email": "user@example.com"
}
Signature#
Generated using RS256 with the private key.
Token Validation Flow#
ForwardAuth Middleware#
Validation Steps#
Extract Token
auth_header = request.headers.get("Authorization") token = auth_header.split(" ")[1] # Bearer TOKEN
Verify Signature
payload = jwt.decode( token, public_key, algorithms=["RS256"], audience=client_id )
Check Revocation
jti = payload["jti"] if redis.exists(f"oauth:revoked:{jti}"): raise TokenRevoked()
Validate Claims
if payload["exp"] < time(): raise TokenExpired() if payload["iss"] != expected_issuer: raise InvalidIssuer()
Client Management (RFC 7592)#
Read Client Configuration#
GET /register/client_7a3d5f2e
Authorization: Bearer reg-4f7d9c2a
Response:
{
"client_id": "client_7a3d5f2e",
"client_name": "My MCP Client",
"redirect_uris": ["http://localhost:8080/callback"],
...
}
Update Client Configuration#
PUT /register/client_7a3d5f2e
Authorization: Bearer reg-4f7d9c2a
Content-Type: application/json
{
"client_name": "Updated Client Name",
"redirect_uris": ["http://localhost:9090/callback"]
}
Delete Client#
DELETE /register/client_7a3d5f2e
Authorization: Bearer reg-4f7d9c2a
Response: 204 No Content
Security Considerations#
PKCE Protection#
Mandatory for all flows
Prevents authorization code interception
S256 challenge method required
State Parameter#
CSRF protection
Cryptographically random
5-minute expiration
Token Security#
RS256 signing (asymmetric)
Short-lived access tokens (30 days)
Secure token storage in Redis
Token revocation support
Client Authentication#
Public clients supported (mobile/SPA)
Client secret for confidential clients
Registration access token for management
Error Handling#
OAuth Errors#
Error |
Description |
HTTP Code |
---|---|---|
|
Malformed request |
400 |
|
Unknown client |
401 |
|
Invalid auth code |
400 |
|
Client not authorized |
403 |
|
Grant type not supported |
400 |
|
Requested scope invalid |
400 |
Token Errors#
Error |
Description |
HTTP Code |
---|---|---|
|
Token validation failed |
401 |
|
Token lacks required scope |
403 |
|
Token past expiration |
401 |
|
Token has been revoked |
401 |
Integration Examples#
Python Client#
import httpx
from authlib.integrations.httpx_client import OAuth2Session
# Client registration
client = OAuth2Session(
client_id=None,
redirect_uri="http://localhost:8080/callback"
)
# Register client
reg_response = await client.post(
"https://auth.example.com/register",
json={
"client_name": "Python MCP Client",
"redirect_uris": ["http://localhost:8080/callback"]
}
)
client_info = reg_response.json()
# Authorization flow
auth_url, state = client.create_authorization_url(
"https://auth.example.com/authorize",
code_challenge=code_challenge,
code_challenge_method="S256"
)
# Token exchange
token = await client.fetch_token(
"https://auth.example.com/token",
authorization_response=callback_url,
code_verifier=code_verifier
)
JavaScript Client#
// Using PKCE
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
// Authorization URL
const params = new URLSearchParams({
client_id: clientId,
redirect_uri: redirectUri,
response_type: 'code',
scope: 'mcp:*',
state: generateState(),
code_challenge: codeChallenge,
code_challenge_method: 'S256'
});
window.location.href = `https://auth.example.com/authorize?${params}`;
// Token exchange
const tokenResponse = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: clientId,
code: authCode,
redirect_uri: redirectUri,
code_verifier: codeVerifier
})
});