Leveraging MCP Client's OAuthClientProvider for Seamless AWS AgentCore Authentication
Overview
Overview
Building on my previous exploration of connecting to MCP servers hosted on AWS AgentCore, I've been working extensively with the native MCP SDK's OAuth Client Provider to streamline authentication workflows. The MCP SDK's built-in OAuth support has evolved significantly, offering robust solutions for both interactive user authentication and machine-to-machine (M2M) flows.
In this follow-up article, I'll share the key improvements and special techniques I've discovered for using the MCP Client's OAuthClientProvider
with AWS AgentCore, including handling AgentCore's unique behavior with 403 responses, implementing M2M authentication flows, and leveraging automatic token refresh capabilities.
What makes this approach particularly compelling is how the native SDK abstracts away much of the OAuth complexity while providing the flexibility needed for enterprise-grade deployments on AWS AgentCore.
Key Improvements Over Manual OAuth Implementation
The native MCP SDK OAuth Client Provider offers several advantages over the manual OAuth implementations I covered in my previous post:
1. Automatic Token Management
- Built-in token storage and refresh mechanisms
- Seamless handling of expired tokens with automatic retry logic
- Support for both
refresh_token
(interactive) andclient_credentials
(M2M) flows
2. AgentCore-Specific Compatibility
- Custom handling of 403 HTTP responses (AgentCore returns 403 instead of 401 for unauthorized requests)
- Proper cross-domain OAuth metadata configuration
- Enhanced error handling and debugging capabilities
3. Dual-Mode Authentication
- Automatic detection of M2M vs Interactive mode based on client configuration
- Single codebase supporting both authentication patterns
- Intelligent scope selection based on OAuth provider type
The AgentCoreOAuthClientProvider
The heart of this improved implementation is a custom OAuth provider that extends the native MCP SDK's OAuthClientProvider
:
1class AgentCoreOAuthClientProvider(OAuthClientProvider):
2 """Custom OAuth provider that triggers on 403 (not just 401) for AgentCore compatibility.
3
4 Supports both interactive OAuth flows and M2M (client_credentials) flows with automatic
5 token refresh for both modes.
6 """
7
8 def __init__(self, *args, **kwargs):
9 super().__init__(*args, **kwargs)
10 self.is_m2m_mode = False # Will be set after client info is available
11
12 def _detect_m2m_mode(self) -> bool:
13 """Detect if we're in M2M mode based on client_secret availability."""
14 return bool(
15 self.context.client_info and
16 self.context.client_info.client_secret and
17 hasattr(self.context, 'client_metadata') and
18 not hasattr(self.context.client_metadata, 'redirect_uris') or
19 not self.context.client_metadata.redirect_uris
20 )
21
22 async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.Request, httpx.Response]:
23 """HTTPX auth flow integration with 403 support and M2M mode."""
24 # ... initialization logic ...
25
26 response = yield request
27
28 # CUSTOM FIX: Trigger OAuth flow on 403 OR 401 (AgentCore returns 403)
29 if response.status_code in (401, 403):
30 # Perform appropriate OAuth flow based on mode
31 if self.is_m2m_mode:
32 # M2M mode: Use client_credentials directly, no browser interaction
33 token_request = await self._get_m2m_token()
34 token_response = yield token_request
35 await self._handle_m2m_token_response(token_response)
36 else:
37 # Interactive mode: Use authorization code flow
38 auth_code, code_verifier = await self._perform_authorization()
39 token_request = await self._exchange_token(auth_code, code_verifier)
40 token_response = yield token_request
41 await self._handle_token_response(token_response)
42
43 # Retry with new tokens
44 self._add_auth_header(request)
45 yield request
Special Tricks for AgentCore Runtime
1. 403 Response Handling
AWS AgentCore returns HTTP 403 (Forbidden) instead of the standard HTTP 401 (Unauthorized) when authentication is required. This is a critical detail that trips up most OAuth implementations:
1# Standard OAuth implementations only handle 401
2if response.status_code == 401:
3 # Trigger OAuth flow
4
5# AgentCore-compatible implementation handles both
6if response.status_code in (401, 403):
7 # Trigger OAuth flow - works with both standard servers and AgentCore
2. Cross-Domain Metadata Configuration
AgentCore MCP servers run on a different domain from the OAuth provider (typically AWS Cognito). This requires manual configuration of protected resource metadata:
1# Extract OAuth server URL from discovery URL
2oauth_server_url = config['discovery_url'].replace('/.well-known/openid_configuration', '')
3
4# Create protected resource metadata pointing to Cognito
5protected_metadata = ProtectedResourceMetadata(
6 resource=PydanticUrl(config['mcp_server_url']),
7 authorization_servers=[PydanticUrl(oauth_server_url)]
8)
9
10# Manually inject the metadata into the OAuth context
11oauth_auth.context.protected_resource_metadata = protected_metadata
12oauth_auth.context.auth_server_url = oauth_server_url
3. Pre-configured Client Information
AWS Cognito doesn't support OAuth dynamic client registration, so we need to pre-configure client information:
1# Pre-configure client info to skip registration
2client_info = OAuthClientInformationFull(
3 client_id=config['client_id'],
4 client_secret=config.get('client_secret'),
5 authorization_endpoint="", # Will be populated during OAuth metadata discovery
6 token_endpoint="", # Will be populated during OAuth metadata discovery
7 redirect_uris=redirect_uris
8)
9await token_storage.set_client_info(client_info)
M2M Authentication Flow Support
One of the most significant improvements is robust support for M2M authentication using the OAuth 2.0 client credentials flow:
Automatic Mode Detection
The system automatically detects whether to use M2M or interactive authentication based on the presence of a client secret:
1# Detect M2M mode based on client_secret presence
2is_m2m_mode = bool(config.get('client_secret'))
3
4if is_m2m_mode:
5 print("🏭 Detected client_secret - using M2M authentication")
6 print("🚀 No user interaction required - fully automated")
7else:
8 print("🔐 No client_secret detected - using interactive authentication")
9 print("🌐 Browser-based user authentication required")
M2M Token Acquisition
The M2M flow bypasses browser-based authorization entirely:
1async def _get_m2m_token(self) -> httpx.Request:
2 """Get M2M access token using client_credentials flow."""
3 token_data = {
4 "grant_type": "client_credentials",
5 "client_id": self.context.client_info.client_id,
6 "client_secret": self.context.client_info.client_secret,
7 }
8
9 # Add scope if specified
10 if self.context.client_metadata.scope:
11 token_data["scope"] = self.context.client_metadata.scope
12
13 return httpx.Request(
14 "POST",
15 token_url,
16 data=token_data,
17 headers={"Content-Type": "application/x-www-form-urlencoded"}
18 )
AWS Cognito M2M Configuration
For AWS Cognito M2M flows, specific configuration is required:
1# For AWS Cognito M2M, configure appropriate scopes
2if 'cognito-idp' in discovery_url.lower():
3 if is_m2m_mode:
4 # Use configured M2M scopes or None for default
5 scope = config['m2m_scopes'] # e.g., "mcp-server/read mcp-server/write"
6 else:
7 scope = 'openid email aws.cognito.signin.user.admin'
Complete Implementation Example
Here's how to use the improved OAuth Client Provider:
1async def test_native_sdk_oauth_flow(config: dict):
2 """Test native MCP SDK OAuth flow with auto-detection of M2M vs interactive mode."""
3 # Detect M2M mode based on client_secret presence
4 is_m2m_mode = bool(config.get('client_secret'))
5
6 # Configure appropriate scopes based on provider and mode
7 if 'cognito-idp' in config['discovery_url'].lower():
8 if is_m2m_mode:
9 scope = config['m2m_scopes'] # Resource server scopes
10 else:
11 scope = 'openid email aws.cognito.signin.user.admin'
12 else:
13 scope = 'openid email profile' if not is_m2m_mode else config['m2m_scopes']
14
15 # Create OAuth client metadata
16 client_metadata = OAuthClientMetadata(
17 client_name="MCP AgentCore OAuth Client",
18 redirect_uris=[AnyUrl("http://localhost:3000")],
19 grant_types=["authorization_code", "refresh_token"],
20 response_types=["code"],
21 scope=scope,
22 )
23
24 # Create token storage with debugging
25 token_storage = DebugTokenStorage()
26
27 # Pre-configure client info
28 client_info = OAuthClientInformationFull(
29 client_id=config['client_id'],
30 client_secret=config.get('client_secret'),
31 authorization_endpoint="",
32 token_endpoint="",
33 redirect_uris=[AnyUrl("http://localhost:3000")]
34 )
35 await token_storage.set_client_info(client_info)
36
37 # Create custom OAuth client provider with AgentCore compatibility
38 oauth_auth = AgentCoreOAuthClientProvider(
39 server_url=config['mcp_server_url'],
40 client_metadata=client_metadata,
41 storage=token_storage,
42 redirect_handler=handle_redirect if not is_m2m_mode else dummy_handler,
43 callback_handler=handle_callback if not is_m2m_mode else dummy_handler,
44 )
45
46 # Configure protected resource metadata for cross-domain support
47 oauth_server_url = config['discovery_url'].replace('/.well-known/openid_configuration', '')
48 protected_metadata = ProtectedResourceMetadata(
49 resource=PydanticUrl(config['mcp_server_url']),
50 authorization_servers=[AnyUrl(oauth_server_url)]
51 )
52 oauth_auth.context.protected_resource_metadata = protected_metadata
53 oauth_auth.context.auth_server_url = oauth_server_url
54
55 # Use the OAuth provider with streamable HTTP client
56 async with streamablehttp_client(config['mcp_server_url'], auth=oauth_auth) as (read, write, _):
57 async with ClientSession(read, write) as session:
58 await session.initialize()
59
60 # List and invoke tools
61 tools_result = await session.list_tools()
62 print(f"Found {len(tools_result.tools)} tools available")
63
64 return True
Configuration and Environment Setup
The improved implementation supports flexible configuration through environment variables:
1# OAuth 2.0 Configuration
2export OAUTH_DISCOVERY_URL="https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123/.well-known/openid_configuration"
3export OAUTH_CLIENT_ID="your-cognito-client-id"
4
5# M2M Mode (optional - enables machine-to-machine authentication)
6export OAUTH_CLIENT_SECRET="your-client-secret"
7export OAUTH_M2M_SCOPES="mcp-server/read mcp-server/write"
8
9# AgentCore Runtime Configuration
10export AGENTCORE_RUNTIME_ARN="arn:aws:bedrock-agentcore:us-west-2:123456789012:runtime/my-server"
11export AGENTCORE_REGION="us-west-2"
12
13# Interactive Mode Testing (optional)
14export OAUTH_TEST_USERNAME="testuser@example.com"
15export OAUTH_TEST_PASSWORD="your-password"
Key Advantages
1. Simplified Integration
The native SDK OAuth provider handles all the complex OAuth state management, token storage, and refresh logic automatically.
2. Production-Ready M2M Support
M2M authentication enables fully automated server-to-server communication without user intervention, perfect for production deployments.
3. AgentCore Compatibility
Custom handling of AgentCore's 403 responses and cross-domain metadata configuration ensures seamless integration.
4. Automatic Token Refresh
Both interactive and M2M modes support automatic token refresh, ensuring long-running applications maintain connectivity.
5. Comprehensive Error Handling
Detailed logging and error handling makes troubleshooting authentication issues much easier.
Troubleshooting Common Issues
M2M Authentication Failures
1# Ensure client_credentials flow is enabled in Cognito
2aws cognito-idp update-user-pool-client \
3 --user-pool-id <your-user-pool-id> \
4 --client-id <your-client-id> \
5 --allowed-o-auth-flows "client_credentials" \
6 --generate-secret
Scope Configuration
For AWS Cognito M2M, you may need to configure resource server scopes:
- Interactive mode:
openid email aws.cognito.signin.user.admin
- M2M mode: Custom resource server scopes like
mcp-server/read mcp-server/write
Cross-Domain Issues
Ensure the protected resource metadata correctly maps your MCP server URL to the OAuth authorization server.
Conclusion
The native MCP SDK's OAuth Client Provider, enhanced with AgentCore-specific compatibility fixes, provides a robust foundation for production MCP client applications. The automatic detection of M2M vs interactive modes, combined with comprehensive error handling and token management, significantly reduces the complexity of integrating with OAuth-protected MCP servers on AWS AgentCore.
The key innovations—handling 403 responses, cross-domain metadata configuration, and dual-mode authentication—make this approach far more reliable than manual OAuth implementations for enterprise deployments.
As the MCP ecosystem continues to evolve, I expect we'll see these patterns become standard practice for production MCP client implementations, particularly in enterprise environments where M2M authentication and automated token management are essential requirements.