workspace-mcp 1.1.7__py3-none-any.whl → 1.1.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
auth/oauth21/config.py ADDED
@@ -0,0 +1,380 @@
1
+ """
2
+ OAuth 2.1 Configuration Schema
3
+
4
+ Configuration classes and validation for OAuth 2.1 authentication setup.
5
+ """
6
+
7
+ import os
8
+ import logging
9
+ from dataclasses import dataclass, field
10
+ from typing import Dict, Any, Optional, List, Union
11
+ from pathlib import Path
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @dataclass
17
+ class OAuth2Config:
18
+ """OAuth 2.1 configuration."""
19
+
20
+ # Authorization Server Configuration
21
+ authorization_server_url: Optional[str] = None
22
+ client_id: Optional[str] = None
23
+ client_secret: Optional[str] = None
24
+
25
+ # Token Configuration
26
+ supported_token_types: List[str] = field(default_factory=lambda: ["jwt", "opaque"])
27
+ token_validation_method: str = "introspection" # or "local"
28
+ expected_audience: Optional[str] = None
29
+
30
+ # Session Management
31
+ session_timeout: int = 3600 # 1 hour
32
+ max_sessions_per_user: int = 10
33
+ session_cleanup_interval: int = 300 # 5 minutes
34
+ enable_session_persistence: bool = False
35
+ session_persistence_file: Optional[str] = None
36
+
37
+ # Security Settings
38
+ enable_pkce: bool = True
39
+ required_scopes: List[str] = field(default_factory=list)
40
+ enable_bearer_passthrough: bool = True
41
+ enable_dynamic_registration: bool = True
42
+
43
+ # Discovery Settings
44
+ resource_url: Optional[str] = None
45
+ discovery_cache_ttl: int = 3600 # 1 hour
46
+ jwks_cache_ttl: int = 3600 # 1 hour
47
+
48
+ # HTTP Settings
49
+ exempt_paths: List[str] = field(default_factory=lambda: [
50
+ "/health", "/oauth2callback", "/.well-known/"
51
+ ])
52
+
53
+ # Development/Debug Settings
54
+ enable_debug_logging: bool = False
55
+ allow_insecure_transport: bool = False
56
+
57
+ def __post_init__(self):
58
+ """Validate configuration after initialization."""
59
+ self._load_from_environment()
60
+ self._validate_config()
61
+
62
+ def _load_from_environment(self):
63
+ """Load configuration from environment variables."""
64
+ # Authorization server settings
65
+ if not self.authorization_server_url:
66
+ self.authorization_server_url = os.getenv("OAUTH2_AUTHORIZATION_SERVER_URL")
67
+
68
+ if not self.client_id:
69
+ self.client_id = os.getenv("OAUTH2_CLIENT_ID") or os.getenv("GOOGLE_OAUTH_CLIENT_ID")
70
+
71
+ if not self.client_secret:
72
+ self.client_secret = os.getenv("OAUTH2_CLIENT_SECRET") or os.getenv("GOOGLE_OAUTH_CLIENT_SECRET")
73
+
74
+ # Token settings
75
+ if env_audience := os.getenv("OAUTH2_EXPECTED_AUDIENCE"):
76
+ self.expected_audience = env_audience
77
+
78
+ if env_validation := os.getenv("OAUTH2_TOKEN_VALIDATION_METHOD"):
79
+ self.token_validation_method = env_validation
80
+
81
+ # Session settings
82
+ if env_timeout := os.getenv("OAUTH2_SESSION_TIMEOUT"):
83
+ try:
84
+ self.session_timeout = int(env_timeout)
85
+ except ValueError:
86
+ logger.warning(f"Invalid OAUTH2_SESSION_TIMEOUT value: {env_timeout}")
87
+
88
+ if env_max_sessions := os.getenv("OAUTH2_MAX_SESSIONS_PER_USER"):
89
+ try:
90
+ self.max_sessions_per_user = int(env_max_sessions)
91
+ except ValueError:
92
+ logger.warning(f"Invalid OAUTH2_MAX_SESSIONS_PER_USER value: {env_max_sessions}")
93
+
94
+ # Security settings
95
+ if env_pkce := os.getenv("OAUTH2_ENABLE_PKCE"):
96
+ self.enable_pkce = env_pkce.lower() in ("true", "1", "yes", "on")
97
+
98
+ if env_passthrough := os.getenv("OAUTH2_ENABLE_BEARER_PASSTHROUGH"):
99
+ self.enable_bearer_passthrough = env_passthrough.lower() in ("true", "1", "yes", "on")
100
+
101
+ if env_scopes := os.getenv("OAUTH2_REQUIRED_SCOPES"):
102
+ self.required_scopes = [scope.strip() for scope in env_scopes.split(",")]
103
+
104
+ # Development settings
105
+ if env_debug := os.getenv("OAUTH2_ENABLE_DEBUG"):
106
+ self.enable_debug_logging = env_debug.lower() in ("true", "1", "yes", "on")
107
+
108
+ if env_insecure := os.getenv("OAUTH2_ALLOW_INSECURE_TRANSPORT"):
109
+ self.allow_insecure_transport = env_insecure.lower() in ("true", "1", "yes", "on")
110
+
111
+ def _validate_config(self):
112
+ """Validate configuration values."""
113
+ errors = []
114
+
115
+ # Validate token types
116
+ valid_token_types = {"jwt", "opaque"}
117
+ invalid_types = set(self.supported_token_types) - valid_token_types
118
+ if invalid_types:
119
+ errors.append(f"Invalid token types: {invalid_types}")
120
+
121
+ # Validate token validation method
122
+ if self.token_validation_method not in ("introspection", "local"):
123
+ errors.append(f"Invalid token_validation_method: {self.token_validation_method}")
124
+
125
+ # Validate session settings
126
+ if self.session_timeout <= 0:
127
+ errors.append("session_timeout must be positive")
128
+
129
+ if self.max_sessions_per_user <= 0:
130
+ errors.append("max_sessions_per_user must be positive")
131
+
132
+ # Validate URLs
133
+ if self.authorization_server_url:
134
+ if not self.authorization_server_url.startswith(("http://", "https://")):
135
+ errors.append("authorization_server_url must be a valid HTTP/HTTPS URL")
136
+
137
+ if errors:
138
+ raise ValueError(f"OAuth2 configuration errors: {'; '.join(errors)}")
139
+
140
+ def is_enabled(self) -> bool:
141
+ """Check if OAuth 2.1 authentication is enabled."""
142
+ return bool(self.client_id)
143
+
144
+ def get_session_persistence_path(self) -> Optional[Path]:
145
+ """Get session persistence file path."""
146
+ if not self.enable_session_persistence:
147
+ return None
148
+
149
+ if self.session_persistence_file:
150
+ return Path(self.session_persistence_file)
151
+
152
+ # Default to user home directory
153
+ home_dir = Path.home()
154
+ return home_dir / ".google_workspace_mcp" / "oauth21_sessions.json"
155
+
156
+ def to_dict(self) -> Dict[str, Any]:
157
+ """Convert configuration to dictionary."""
158
+ return {
159
+ "authorization_server_url": self.authorization_server_url,
160
+ "client_id": self.client_id,
161
+ "client_secret": "***" if self.client_secret else None, # Redact secret
162
+ "supported_token_types": self.supported_token_types,
163
+ "token_validation_method": self.token_validation_method,
164
+ "expected_audience": self.expected_audience,
165
+ "session_timeout": self.session_timeout,
166
+ "max_sessions_per_user": self.max_sessions_per_user,
167
+ "enable_pkce": self.enable_pkce,
168
+ "required_scopes": self.required_scopes,
169
+ "enable_bearer_passthrough": self.enable_bearer_passthrough,
170
+ "enable_dynamic_registration": self.enable_dynamic_registration,
171
+ "resource_url": self.resource_url,
172
+ "exempt_paths": self.exempt_paths,
173
+ "enable_debug_logging": self.enable_debug_logging,
174
+ "allow_insecure_transport": self.allow_insecure_transport,
175
+ }
176
+
177
+
178
+ @dataclass
179
+ class AuthConfig:
180
+ """Complete authentication configuration including OAuth 2.1 and legacy settings."""
181
+
182
+ # OAuth 2.1 Configuration
183
+ oauth2: Optional[OAuth2Config] = None
184
+
185
+ # Legacy Authentication Settings (for backward compatibility)
186
+ enable_legacy_auth: bool = True
187
+ legacy_credentials_dir: Optional[str] = None
188
+
189
+ # Global Settings
190
+ single_user_mode: bool = False
191
+ default_user_email: Optional[str] = None
192
+
193
+ def __post_init__(self):
194
+ """Initialize configuration."""
195
+ self._load_global_settings()
196
+
197
+ # Initialize OAuth2 config if not provided but environment suggests it's needed
198
+ if not self.oauth2 and self._should_enable_oauth2():
199
+ self.oauth2 = OAuth2Config()
200
+
201
+ def _load_global_settings(self):
202
+ """Load global authentication settings."""
203
+ # Single user mode
204
+ if env_single_user := os.getenv("MCP_SINGLE_USER_MODE"):
205
+ self.single_user_mode = env_single_user.lower() in ("true", "1", "yes", "on")
206
+
207
+ # Default user email
208
+ if not self.default_user_email:
209
+ self.default_user_email = os.getenv("USER_GOOGLE_EMAIL")
210
+
211
+ # Legacy settings
212
+ if env_legacy := os.getenv("OAUTH2_ENABLE_LEGACY_AUTH"):
213
+ self.enable_legacy_auth = env_legacy.lower() in ("true", "1", "yes", "on")
214
+
215
+ if not self.legacy_credentials_dir:
216
+ self.legacy_credentials_dir = os.getenv("GOOGLE_MCP_CREDENTIALS_DIR")
217
+
218
+ def _should_enable_oauth2(self) -> bool:
219
+ """Check if OAuth 2.1 should be enabled based on environment."""
220
+ oauth2_env_vars = [
221
+ "OAUTH2_CLIENT_ID",
222
+ "GOOGLE_OAUTH_CLIENT_ID",
223
+ "OAUTH2_AUTHORIZATION_SERVER_URL",
224
+ "OAUTH2_ENABLE_BEARER_PASSTHROUGH",
225
+ ]
226
+
227
+ return any(os.getenv(var) for var in oauth2_env_vars)
228
+
229
+ def is_oauth2_enabled(self) -> bool:
230
+ """Check if OAuth 2.1 is enabled."""
231
+ return self.oauth2 is not None and self.oauth2.is_enabled()
232
+
233
+ def get_effective_auth_mode(self) -> str:
234
+ """Get the effective authentication mode."""
235
+ if self.single_user_mode:
236
+ return "single_user"
237
+ elif self.is_oauth2_enabled():
238
+ if self.enable_legacy_auth:
239
+ return "oauth2_with_legacy_fallback"
240
+ else:
241
+ return "oauth2_only"
242
+ elif self.enable_legacy_auth:
243
+ return "legacy_only"
244
+ else:
245
+ return "disabled"
246
+
247
+ def to_dict(self) -> Dict[str, Any]:
248
+ """Convert configuration to dictionary."""
249
+ result = {
250
+ "oauth2": self.oauth2.to_dict() if self.oauth2 else None,
251
+ "enable_legacy_auth": self.enable_legacy_auth,
252
+ "legacy_credentials_dir": self.legacy_credentials_dir,
253
+ "single_user_mode": self.single_user_mode,
254
+ "default_user_email": self.default_user_email,
255
+ "effective_auth_mode": self.get_effective_auth_mode(),
256
+ }
257
+
258
+ return result
259
+
260
+
261
+ def create_auth_config(
262
+ oauth2_config: Optional[Dict[str, Any]] = None,
263
+ **kwargs
264
+ ) -> AuthConfig:
265
+ """
266
+ Create authentication configuration.
267
+
268
+ Args:
269
+ oauth2_config: OAuth 2.1 configuration dictionary
270
+ **kwargs: Additional configuration options
271
+
272
+ Returns:
273
+ Authentication configuration
274
+ """
275
+ oauth2 = None
276
+ if oauth2_config:
277
+ oauth2 = OAuth2Config(**oauth2_config)
278
+
279
+ return AuthConfig(oauth2=oauth2, **kwargs)
280
+
281
+
282
+ def create_default_oauth2_config() -> OAuth2Config:
283
+ """
284
+ Create default OAuth 2.1 configuration for Google Workspace.
285
+
286
+ Returns:
287
+ OAuth 2.1 configuration with Google Workspace defaults
288
+ """
289
+ return OAuth2Config(
290
+ authorization_server_url="https://accounts.google.com",
291
+ supported_token_types=["jwt"],
292
+ token_validation_method="local", # Use local JWT validation for Google
293
+ expected_audience=None, # Will be set based on client_id
294
+ required_scopes=[
295
+ "https://www.googleapis.com/auth/userinfo.email",
296
+ "https://www.googleapis.com/auth/userinfo.profile",
297
+ ],
298
+ enable_pkce=True,
299
+ enable_bearer_passthrough=True,
300
+ resource_url="https://www.googleapis.com",
301
+ )
302
+
303
+
304
+ def load_config_from_file(config_path: Union[str, Path]) -> AuthConfig:
305
+ """
306
+ Load authentication configuration from file.
307
+
308
+ Args:
309
+ config_path: Path to configuration file (JSON or TOML)
310
+
311
+ Returns:
312
+ Authentication configuration
313
+
314
+ Raises:
315
+ FileNotFoundError: If config file doesn't exist
316
+ ValueError: If config format is invalid
317
+ """
318
+ config_path = Path(config_path)
319
+
320
+ if not config_path.exists():
321
+ raise FileNotFoundError(f"Configuration file not found: {config_path}")
322
+
323
+ try:
324
+ if config_path.suffix.lower() == ".json":
325
+ import json
326
+ with open(config_path) as f:
327
+ config_data = json.load(f)
328
+ elif config_path.suffix.lower() in (".toml", ".tml"):
329
+ import tomlkit
330
+ with open(config_path) as f:
331
+ config_data = tomlkit.load(f)
332
+ else:
333
+ raise ValueError(f"Unsupported configuration file format: {config_path.suffix}")
334
+
335
+ # Extract OAuth 2.1 config if present
336
+ oauth2_config = None
337
+ if "oauth2" in config_data:
338
+ oauth2_config = config_data.pop("oauth2")
339
+
340
+ return create_auth_config(oauth2_config=oauth2_config, **config_data)
341
+
342
+ except Exception as e:
343
+ raise ValueError(f"Failed to load configuration from {config_path}: {e}")
344
+
345
+
346
+ def get_config_summary(config: AuthConfig) -> str:
347
+ """
348
+ Get a human-readable summary of the authentication configuration.
349
+
350
+ Args:
351
+ config: Authentication configuration
352
+
353
+ Returns:
354
+ Configuration summary string
355
+ """
356
+ lines = [
357
+ "Authentication Configuration Summary:",
358
+ f" Mode: {config.get_effective_auth_mode()}",
359
+ f" Single User Mode: {config.single_user_mode}",
360
+ ]
361
+
362
+ if config.oauth2:
363
+ lines.extend([
364
+ " OAuth 2.1 Settings:",
365
+ f" Authorization Server: {config.oauth2.authorization_server_url or 'Not configured'}",
366
+ f" Client ID: {config.oauth2.client_id or 'Not configured'}",
367
+ f" Token Types: {', '.join(config.oauth2.supported_token_types)}",
368
+ f" Session Timeout: {config.oauth2.session_timeout}s",
369
+ f" Bearer Passthrough: {config.oauth2.enable_bearer_passthrough}",
370
+ f" PKCE Enabled: {config.oauth2.enable_pkce}",
371
+ ])
372
+
373
+ if config.enable_legacy_auth:
374
+ lines.extend([
375
+ " Legacy Authentication:",
376
+ f" Enabled: {config.enable_legacy_auth}",
377
+ f" Credentials Dir: {config.legacy_credentials_dir or 'Default'}",
378
+ ])
379
+
380
+ return "\n".join(lines)
@@ -0,0 +1,232 @@
1
+ """
2
+ Authorization Server Discovery Module
3
+
4
+ Implements RFC9728 Protected Resource Metadata and RFC8414 Authorization Server Metadata discovery
5
+ for OAuth 2.1 compliance.
6
+ """
7
+
8
+ import logging
9
+ from typing import Dict, Any, Optional, List
10
+
11
+ import aiohttp
12
+ from cachetools import TTLCache
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class AuthorizationServerDiscovery:
18
+ """Implements RFC9728 Protected Resource Metadata and RFC8414 AS Metadata discovery."""
19
+
20
+ def __init__(
21
+ self,
22
+ resource_url: Optional[str] = None,
23
+ cache_ttl: int = 3600,
24
+ max_cache_size: int = 100,
25
+ ):
26
+ """
27
+ Initialize the discovery service.
28
+
29
+ Args:
30
+ resource_url: The protected resource URL for this server
31
+ cache_ttl: Cache time-to-live in seconds
32
+ max_cache_size: Maximum number of cached entries
33
+ """
34
+ self.resource_url = resource_url or "https://www.googleapis.com"
35
+ self.cache = TTLCache(maxsize=max_cache_size, ttl=cache_ttl)
36
+ self._session: Optional[aiohttp.ClientSession] = None
37
+
38
+ async def _get_session(self) -> aiohttp.ClientSession:
39
+ """Get or create HTTP session."""
40
+ if self._session is None or self._session.closed:
41
+ self._session = aiohttp.ClientSession(
42
+ timeout=aiohttp.ClientTimeout(total=30),
43
+ headers={"User-Agent": "MCP-OAuth2.1-Client/1.0"},
44
+ )
45
+ return self._session
46
+
47
+ async def close(self):
48
+ """Clean up resources."""
49
+ if self._session and not self._session.closed:
50
+ await self._session.close()
51
+
52
+ async def get_protected_resource_metadata(self) -> Dict[str, Any]:
53
+ """
54
+ Return Protected Resource Metadata per RFC9728.
55
+
56
+ Returns:
57
+ Protected resource metadata including authorization_servers list
58
+ """
59
+ cache_key = f"prm:{self.resource_url}"
60
+ if cache_key in self.cache:
61
+ logger.debug(f"Using cached protected resource metadata for {self.resource_url}")
62
+ return self.cache[cache_key]
63
+
64
+ metadata = {
65
+ "resource": self.resource_url,
66
+ "authorization_servers": [
67
+ "https://accounts.google.com",
68
+ "https://oauth2.googleapis.com",
69
+ ],
70
+ "scopes_supported": [
71
+ "https://www.googleapis.com/auth/userinfo.email",
72
+ "https://www.googleapis.com/auth/userinfo.profile",
73
+ "https://www.googleapis.com/auth/calendar",
74
+ "https://www.googleapis.com/auth/calendar.readonly",
75
+ "https://www.googleapis.com/auth/drive",
76
+ "https://www.googleapis.com/auth/drive.readonly",
77
+ "https://www.googleapis.com/auth/gmail.readonly",
78
+ "https://www.googleapis.com/auth/gmail.modify",
79
+ "https://www.googleapis.com/auth/gmail.send",
80
+ "https://www.googleapis.com/auth/documents",
81
+ "https://www.googleapis.com/auth/documents.readonly",
82
+ "https://www.googleapis.com/auth/spreadsheets",
83
+ "https://www.googleapis.com/auth/spreadsheets.readonly",
84
+ "https://www.googleapis.com/auth/presentations",
85
+ "https://www.googleapis.com/auth/presentations.readonly",
86
+ "https://www.googleapis.com/auth/chat.spaces",
87
+ "https://www.googleapis.com/auth/chat.messages",
88
+ "https://www.googleapis.com/auth/forms.body",
89
+ "https://www.googleapis.com/auth/forms.responses.readonly",
90
+ "https://www.googleapis.com/auth/tasks",
91
+ "https://www.googleapis.com/auth/tasks.readonly",
92
+ ],
93
+ "bearer_methods_supported": ["header"],
94
+ "resource_documentation": "https://developers.google.com/workspace",
95
+ }
96
+
97
+ self.cache[cache_key] = metadata
98
+ logger.info(f"Generated protected resource metadata for {self.resource_url}")
99
+ return metadata
100
+
101
+ async def get_authorization_server_metadata(self, as_url: str) -> Dict[str, Any]:
102
+ """
103
+ Fetch and cache Authorization Server metadata per RFC8414.
104
+
105
+ Args:
106
+ as_url: Authorization server URL
107
+
108
+ Returns:
109
+ Authorization server metadata dictionary
110
+
111
+ Raises:
112
+ aiohttp.ClientError: If the metadata cannot be fetched
113
+ """
114
+ cache_key = f"asm:{as_url}"
115
+ if cache_key in self.cache:
116
+ logger.debug(f"Using cached authorization server metadata for {as_url}")
117
+ return self.cache[cache_key]
118
+
119
+ # Try standard discovery endpoints
120
+ discovery_urls = [
121
+ f"{as_url}/.well-known/oauth-authorization-server",
122
+ f"{as_url}/.well-known/openid_configuration",
123
+ ]
124
+
125
+ session = await self._get_session()
126
+
127
+ for discovery_url in discovery_urls:
128
+ try:
129
+ logger.debug(f"Attempting to fetch metadata from {discovery_url}")
130
+ async with session.get(discovery_url) as response:
131
+ if response.status == 200:
132
+ metadata = await response.json()
133
+
134
+ # Validate required fields per RFC8414
135
+ required_fields = ["issuer", "authorization_endpoint"]
136
+ if all(field in metadata for field in required_fields):
137
+ # Ensure OAuth 2.1 compliance fields
138
+ metadata.setdefault("code_challenge_methods_supported", ["S256"])
139
+ metadata.setdefault("pkce_required", True)
140
+
141
+ self.cache[cache_key] = metadata
142
+ logger.info(f"Fetched authorization server metadata from {discovery_url}")
143
+ return metadata
144
+ else:
145
+ logger.warning(f"Invalid metadata from {discovery_url}: missing required fields")
146
+
147
+ except Exception as e:
148
+ logger.debug(f"Failed to fetch from {discovery_url}: {e}")
149
+ continue
150
+
151
+ # If discovery fails, return default Google metadata
152
+ logger.warning(f"Could not discover metadata for {as_url}, using defaults")
153
+ default_metadata = self._get_default_google_metadata(as_url)
154
+ self.cache[cache_key] = default_metadata
155
+ return default_metadata
156
+
157
+ def _get_default_google_metadata(self, as_url: str) -> Dict[str, Any]:
158
+ """Return default Google OAuth 2.0 metadata."""
159
+ return {
160
+ "issuer": as_url,
161
+ "authorization_endpoint": f"{as_url}/o/oauth2/v2/auth",
162
+ "token_endpoint": f"{as_url}/token",
163
+ "userinfo_endpoint": f"{as_url}/oauth2/v2/userinfo",
164
+ "revocation_endpoint": f"{as_url}/revoke",
165
+ "jwks_uri": f"{as_url}/oauth2/v3/certs",
166
+ "introspection_endpoint": f"{as_url}/introspect",
167
+ "response_types_supported": ["code"],
168
+ "response_modes_supported": ["query", "fragment"],
169
+ "grant_types_supported": ["authorization_code", "refresh_token"],
170
+ "code_challenge_methods_supported": ["S256"],
171
+ "pkce_required": True,
172
+ "scopes_supported": [
173
+ "openid",
174
+ "email",
175
+ "profile",
176
+ "https://www.googleapis.com/auth/userinfo.email",
177
+ "https://www.googleapis.com/auth/userinfo.profile",
178
+ ],
179
+ "token_endpoint_auth_methods_supported": [
180
+ "client_secret_basic",
181
+ "client_secret_post",
182
+ ],
183
+ "claims_supported": ["iss", "sub", "aud", "exp", "iat", "email", "email_verified"],
184
+ "request_uri_parameter_supported": False,
185
+ "require_request_uri_registration": False,
186
+ }
187
+
188
+ async def discover_authorization_servers(self) -> List[Dict[str, Any]]:
189
+ """
190
+ Discover all authorization servers for this protected resource.
191
+
192
+ Returns:
193
+ List of authorization server metadata dictionaries
194
+ """
195
+ prm = await self.get_protected_resource_metadata()
196
+ servers = []
197
+
198
+ for as_url in prm.get("authorization_servers", []):
199
+ try:
200
+ as_metadata = await self.get_authorization_server_metadata(as_url)
201
+ servers.append(as_metadata)
202
+ except Exception as e:
203
+ logger.error(f"Failed to discover authorization server {as_url}: {e}")
204
+ continue
205
+
206
+ return servers
207
+
208
+ def is_valid_authorization_server(self, as_url: str) -> bool:
209
+ """
210
+ Check if the given URL is a valid authorization server for this resource.
211
+
212
+ Args:
213
+ as_url: Authorization server URL to validate
214
+
215
+ Returns:
216
+ True if the server is valid for this resource
217
+ """
218
+ try:
219
+ # Get cached metadata without making network calls
220
+ cache_key = f"prm:{self.resource_url}"
221
+ if cache_key in self.cache:
222
+ prm = self.cache[cache_key]
223
+ return as_url in prm.get("authorization_servers", [])
224
+ except Exception:
225
+ pass
226
+
227
+ # Default to allowing Google servers
228
+ google_servers = [
229
+ "https://accounts.google.com",
230
+ "https://oauth2.googleapis.com",
231
+ ]
232
+ return as_url in google_servers