OAuth 2.0 Authorization Code Flow UI Implementation Design¶
Version: 1.0 Status: Design Document Date: December 2024 Related: OAuth Design Document
Executive Summary¶
This document outlines the design for implementing OAuth 2.0 Authorization Code flow with user consent in the MCP Gateway UI. The implementation will extend the existing OAuth infrastructure to support user delegation flows, token storage, and automatic token refresh, enabling agents to act on behalf of users with proper consent and scoped permissions.
Current State Analysis¶
Existing Implementation¶
- β OAuth Manager service with Client Credentials flow
- β Basic Authorization Code flow support in OAuth Manager
- β OAuth configuration fields in Gateway creation UI
- β
OAuth callback endpoint (
/oauth/callback
) - β
Database schema with
oauth_config
JSON field - β Client secret encryption/decryption
Current Limitations¶
- β No token storage mechanism for Authorization Code flow
- β No refresh token handling
- β Incomplete UI flow for user consent
- β No token expiration management
- β Limited error handling for OAuth flows
Architecture Overview¶
graph TD
subgraph "MCP Gateway UI"
A[Gateway Configuration]
B[OAuth Authorization Flow]
C[Token Management]
D[User Consent Interface]
end
subgraph "Backend Services"
E[OAuth Manager]
F[Token Storage Service]
G[Gateway Service]
end
subgraph "Database"
H[Gateway Table]
I[OAuth Tokens Table]
end
subgraph "External"
J[OAuth Provider]
K[User Browser]
end
A --> E
B --> E
E --> F
F --> I
G --> F
B --> K
K --> J
J --> B
E --> J
Database Schema Changes¶
New OAuth Tokens Table¶
CREATE TABLE oauth_tokens (
id VARCHAR(36) PRIMARY KEY DEFAULT (uuid()),
gateway_id VARCHAR(36) NOT NULL,
user_id VARCHAR(255) NOT NULL, -- OAuth provider user ID
access_token TEXT NOT NULL,
refresh_token TEXT,
token_type VARCHAR(50) DEFAULT 'Bearer',
expires_at TIMESTAMP,
scopes JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (gateway_id) REFERENCES gateways(id) ON DELETE CASCADE,
UNIQUE KEY unique_gateway_user (gateway_id, user_id)
);
-- Index for efficient token lookup
CREATE INDEX idx_oauth_tokens_gateway_user ON oauth_tokens(gateway_id, user_id);
CREATE INDEX idx_oauth_tokens_expires ON oauth_tokens(expires_at);
Modified Gateway Table¶
-- Add new fields to existing oauth_config JSON structure
ALTER TABLE gateways
MODIFY COLUMN oauth_config JSON COMMENT 'OAuth 2.0 configuration including grant_type, client_id, encrypted client_secret, URLs, scopes, and token management settings';
-- Updated oauth_config structure:
{
"grant_type": "client_credentials|authorization_code",
"client_id": "string",
"client_secret": "encrypted_string",
"authorization_url": "string",
"token_url": "string",
"redirect_uri": "string",
"scopes": ["scope1", "scope2"],
"token_management": {
"store_tokens": true,
"auto_refresh": true,
"refresh_threshold_seconds": 300
}
}
Core Components¶
1. Token Storage Service¶
Location: mcpgateway/services/token_storage_service.py
from datetime import datetime, timedelta
from typing import Optional, Dict, Any, List
from sqlalchemy.orm import Session
from mcpgateway.db import OAuthToken, DbGateway
from mcpgateway.utils.oauth_encryption import get_oauth_encryption
class TokenStorageService:
"""Manages OAuth token storage and retrieval."""
def __init__(self, db: Session):
self.db = db
self.encryption = get_oauth_encryption()
async def store_tokens(
self,
gateway_id: str,
user_id: str,
access_token: str,
refresh_token: Optional[str],
expires_in: int,
scopes: List[str]
) -> OAuthToken:
"""Store OAuth tokens for a gateway-user combination."""
# Encrypt sensitive tokens
encrypted_access = self.encryption.encrypt_secret(access_token)
encrypted_refresh = None
if refresh_token:
encrypted_refresh = self.encryption.encrypt_secret(refresh_token)
# Calculate expiration
expires_at = datetime.utcnow() + timedelta(seconds=expires_in)
# Create or update token record
token_record = self.db.query(OAuthToken).filter(
OAuthToken.gateway_id == gateway_id,
OAuthToken.user_id == user_id
).first()
if token_record:
# Update existing record
token_record.access_token = encrypted_access
token_record.refresh_token = encrypted_refresh
token_record.expires_at = expires_at
token_record.scopes = scopes
token_record.updated_at = datetime.utcnow()
else:
# Create new record
token_record = OAuthToken(
gateway_id=gateway_id,
user_id=user_id,
access_token=encrypted_access,
refresh_token=encrypted_refresh,
expires_at=expires_at,
scopes=scopes
)
self.db.add(token_record)
self.db.commit()
return token_record
async def get_valid_token(
self,
gateway_id: str,
user_id: str
) -> Optional[str]:
"""Get a valid access token, refreshing if necessary."""
token_record = self.db.query(OAuthToken).filter(
OAuthToken.gateway_id == gateway_id,
OAuthToken.user_id == user_id
).first()
if not token_record:
return None
# Check if token is expired or near expiration
if self._is_token_expired(token_record):
if token_record.refresh_token:
# Attempt to refresh token
new_token = await self._refresh_access_token(token_record)
if new_token:
return new_token
return None
# Decrypt and return valid token
return self.encryption.decrypt_secret(token_record.access_token)
async def _refresh_access_token(self, token_record: OAuthToken) -> Optional[str]:
"""Refresh an expired access token using refresh token."""
# Implementation for token refresh
pass
def _is_token_expired(self, token_record: OAuthToken, threshold_seconds: int = 300) -> bool:
"""Check if token is expired or near expiration."""
return datetime.utcnow() + timedelta(seconds=threshold_seconds) >= token_record.expires_at
```
2. Enhanced OAuth Manager¶
Location: mcpgateway/services/oauth_manager.py
python class OAuthManager: """Enhanced OAuth Manager with token storage support.""" def __init__(self, token_storage: TokenStorageService): self.token_storage = token_storage async def initiate_authorization_code_flow( self, gateway_id: str, credentials: Dict[str, Any] ) -> Dict[str, str]: """Initiate Authorization Code flow and return authorization URL.""" # Generate state parameter for CSRF protection state = self._generate_state(gateway_id) # Store state in session/cache for validation await self._store_authorization_state(gateway_id, state) # Generate authorization URL auth_url, _ = self._create_authorization_url(credentials, state) return { 'authorization_url': auth_url, 'state': state, 'gateway_id': gateway_id }
async def complete_authorization_code_flow(
self,
gateway_id: str,
code: str,
state: str,
credentials: Dict[str, Any]
) -> Dict[str, Any]:
"""Complete Authorization Code flow and store tokens."""
# Validate state parameter
if not await self._validate_authorization_state(gateway_id, state):
raise OAuthError("Invalid state parameter")
# Exchange code for tokens
token_response = await self._exchange_code_for_tokens(credentials, code)
# Extract user information from token response
user_id = self._extract_user_id(token_response, credentials)
# Store tokens
token_record = await self.token_storage.store_tokens(
gateway_id=gateway_id,
user_id=user_id,
access_token=token_response['access_token'],
refresh_token=token_response.get('refresh_token'),
expires_in=token_response.get('expires_in', 3600),
scopes=token_response.get('scope', '').split()
)
return {
'success': True,
'user_id': user_id,
'expires_at': token_record.expires_at.isoformat()
}
async def get_access_token_for_user(
self,
gateway_id: str,
user_id: str
) -> Optional[str]:
"""Get valid access token for a specific user."""
return await self.token_storage.get_valid_token(gateway_id, user_id)
```
3. OAuth Callback Handler¶
Location: mcpgateway/routers/oauth_router.py
python from fastapi import APIRouter, Depends, Request, HTTPException from fastapi.responses import RedirectResponse, HTMLResponse oauth_router = APIRouter(prefix="/oauth", tags=["oauth"]) @oauth_router.get("/authorize/{gateway_id}") async def initiate_oauth_flow( gateway_id: str, request: Request, db: Session = Depends(get_db) ) -> RedirectResponse: """Initiate OAuth Authorization Code flow.""" # Get gateway configuration gateway = db.query(DbGateway).filter(DbGateway.id == gateway_id).first() if not gateway or not gateway.oauth_config: raise HTTPException(status_code=404, detail="Gateway not found or not configured for OAuth") # Initiate OAuth flow oauth_manager = OAuthManager(TokenStorageService(db)) auth_data = await oauth_manager.initiate_authorization_code_flow( gateway_id, gateway.oauth_config ) # Redirect user to OAuth provider return RedirectResponse(url=auth_data['authorization_url'])
@oauth_router.get("/callback") async def oauth_callback( code: str, state: str, gateway_id: str, request: Request, db: Session = Depends(get_db) ) -> HTMLResponse: """Handle OAuth callback and complete authorization."""
try:
# Complete OAuth flow
oauth_manager = OAuthManager(TokenStorageService(db))
gateway = db.query(DbGateway).filter(DbGateway.id == gateway_id).first()
result = await oauth_manager.complete_authorization_code_flow(
gateway_id, code, state, gateway.oauth_config
)
# Return success page with option to return to admin
return HTMLResponse(content=f"""
<!DOCTYPE html>
<html>
<head><title>OAuth Authorization Successful</title></head>
<body>
<h1>β
OAuth Authorization Successful</h1>
<p>Gateway: {gateway.name}</p>
<p>User: {result['user_id']}</p>
<p>Expires: {result['expires_at']}</p>
<a href="/admin#gateways">Return to Admin Panel</a>
</body>
</html>
""")
except Exception as e:
return HTMLResponse(content=f"""
<!DOCTYPE html>
<html>
<head><title>OAuth Authorization Failed</title></head>
<body>
<h1>β OAuth Authorization Failed</h1>
<p>Error: {str(e)}</p>
<a href="/admin#gateways">Return to Admin Panel</a>
</body>
</html>
""", status_code=400)
```
UI Implementation¶
1. Enhanced Gateway Creation Form¶
File: mcpgateway/templates/admin.html
html <!-- OAuth Configuration Fields --> <div id="auth-oauth-fields-gw" style="display: none"> <div class="space-y-4"> <div> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300"> Grant Type </label> <select name="oauth_grant_type" id="oauth-grant-type-gw" onchange="toggleOAuthFields()" class="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-900 dark:placeholder-gray-300 dark:text-gray-300" > <option value="client_credentials">Client Credentials (Machine-to-Machine)</option> <option value="authorization_code">Authorization Code (User Delegation)</option> </select> </div> <!-- Common OAuth fields --> <div> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300"> Client ID </label> <input type="text" name="oauth_client_id" class="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-900 dark:placeholder-gray-300 dark:text-gray-300" placeholder="Your OAuth client ID" /> </div>
<!-- Authorization Code specific fields -->
<div id="oauth-auth-code-fields-gw" style="display: none">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Authorization URL
</label>
<input
type="url"
name="oauth_authorization_url"
class="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-900 dark:placeholder-gray-300 dark:text-gray-300"
placeholder="https://oauth.example.com/authorize"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Redirect URI
</label>
<input
type="url"
name="oauth_redirect_uri"
class="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-900 dark:placeholder-gray-300 dark:text-gray-300"
placeholder="https://gateway.example.com/oauth/callback"
/>
<p class="mt-1 text-sm text-gray-500">
This must match the redirect URI configured in your OAuth application
</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Token Management
</label>
<div class="mt-2 space-y-2">
<label class="flex items-center">
<input
type="checkbox"
name="oauth_store_tokens"
checked
class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">
Store access tokens for reuse
</span>
</label>
<label class="flex items-center">
<input
type="checkbox"
name="oauth_auto_refresh"
checked
class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
/>
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">
Automatically refresh expired tokens
</span>
</label>
</div>
</div>
</div>