mcp_auth_example.py
1 #!/usr/bin/env python3 2 """ 3 MCP Auth Example 4 5 Demonstrates the OAuth 2.1 and OIDC Discovery per MCP 2025-11-25 specification. 6 7 Features: 8 - OAuth 2.1 with PKCE 9 - OpenID Connect Discovery 10 - API Key authentication 11 - Scope management 12 13 Usage: 14 python mcp_auth_example.py 15 """ 16 17 import asyncio 18 from praisonai.mcp_server.auth.oauth import OAuthConfig, OAuthManager 19 from praisonai.mcp_server.auth.oidc import OIDCDiscovery 20 from praisonai.mcp_server.auth.api_key import APIKeyAuth 21 from praisonai.mcp_server.auth.scopes import ScopeManager 22 23 24 async def main(): 25 print("=" * 60) 26 print("MCP Auth Example (2025-11-25 Specification)") 27 print("=" * 60) 28 29 # 1. OIDC Discovery 30 print("\n1. OpenID Connect Discovery") 31 print("-" * 40) 32 33 discovery = OIDCDiscovery() 34 35 # Discover Google's OIDC configuration 36 issuer = "https://accounts.google.com" 37 print(f" Discovering OIDC config from: {issuer}") 38 39 config = await discovery.discover(issuer) 40 if config: 41 print(f" ✓ Issuer: {config.issuer}") 42 print(f" ✓ Authorization endpoint: {config.authorization_endpoint}") 43 print(f" ✓ Token endpoint: {config.token_endpoint}") 44 print(f" ✓ JWKS URI: {config.jwks_uri}") 45 else: 46 print(" ✗ Discovery failed") 47 48 # 2. OAuth 2.1 Configuration 49 print("\n2. OAuth 2.1 Configuration") 50 print("-" * 40) 51 52 oauth_config = OAuthConfig( 53 authorization_endpoint="https://auth.example.com/authorize", 54 token_endpoint="https://auth.example.com/token", 55 client_id="my-mcp-client", 56 default_scopes=["openid", "profile", "tools:read"], 57 use_pkce=True, # PKCE is required for OAuth 2.1 58 ) 59 60 oauth = OAuthManager(oauth_config) 61 62 # Create authorization URL with PKCE 63 auth_url, auth_request = oauth.create_authorization_url( 64 scopes=["openid", "profile", "tools:read", "tools:call"], 65 ) 66 67 print(f" Authorization URL: {auth_url[:80]}...") 68 print(f" State: {auth_request.state}") 69 print(f" PKCE code verifier: {auth_request.code_verifier[:20]}...") 70 print(f" PKCE code challenge: {auth_request.code_challenge[:20]}...") 71 72 # 3. API Key Authentication 73 print("\n3. API Key Authentication") 74 print("-" * 40) 75 76 api_key_auth = APIKeyAuth(allow_env_key=False) 77 78 # Generate a new API key 79 raw_key, api_key = api_key_auth.generate_key( 80 name="demo-key", 81 scopes=["tools:read", "tools:call", "resources:read"], 82 ) 83 84 print(f" Generated key: {raw_key[:20]}...") 85 print(f" Key ID: {api_key.key_id}") 86 print(f" Key name: {api_key.name}") 87 print(f" Scopes: {api_key.scopes}") 88 89 # Validate the key 90 is_valid, validated_key = api_key_auth.validate(raw_key) 91 print(f"\n Validation result: {'✓ Valid' if is_valid else '✗ Invalid'}") 92 93 # Validate via header 94 is_valid_header, _ = api_key_auth.validate_header(f"Bearer {raw_key}") 95 print(f" Header validation: {'✓ Valid' if is_valid_header else '✗ Invalid'}") 96 97 # 4. Scope Management 98 print("\n4. Scope Management") 99 print("-" * 40) 100 101 scope_manager = ScopeManager() 102 103 # Check if required scopes are granted 104 required = ["tools:read"] 105 granted = ["tools:call"] # tools:call implies tools:read 106 107 is_valid, challenge = scope_manager.validate_scopes(required, granted) 108 print(f" Required: {required}") 109 print(f" Granted: {granted}") 110 print(f" Valid: {'✓' if is_valid else '✗'}") 111 112 # Expand scopes (show implied scopes) 113 expanded = scope_manager.expand_scopes(["tools:call"]) 114 print(f"\n Expanded 'tools:call': {expanded}") 115 116 # Admin scope expands to all 117 admin_expanded = scope_manager.expand_scopes(["admin"]) 118 print(f" Expanded 'admin': {list(admin_expanded)[:5]}...") 119 120 # 5. WWW-Authenticate Challenge 121 print("\n5. WWW-Authenticate Challenge") 122 print("-" * 40) 123 124 challenge_header = oauth.create_www_authenticate_challenge( 125 required_scopes=["admin"], 126 error="insufficient_scope", 127 error_description="Admin access required", 128 ) 129 print(f" Header: {challenge_header}") 130 131 print("\n" + "=" * 60) 132 print("Auth Example Complete!") 133 print("=" * 60) 134 135 136 if __name__ == "__main__": 137 asyncio.run(main())