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) and client_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.

Resources