workspace-mcp 1.1.7__tar.gz → 1.1.9__tar.gz

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.
Files changed (55) hide show
  1. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/PKG-INFO +3 -2
  2. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/README.md +1 -1
  3. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/auth/google_auth.py +1 -1
  4. workspace_mcp-1.1.9/auth/oauth21/__init__.py +108 -0
  5. workspace_mcp-1.1.9/auth/oauth21/compat.py +422 -0
  6. workspace_mcp-1.1.9/auth/oauth21/config.py +380 -0
  7. workspace_mcp-1.1.9/auth/oauth21/discovery.py +232 -0
  8. workspace_mcp-1.1.9/auth/oauth21/example_config.py +303 -0
  9. workspace_mcp-1.1.9/auth/oauth21/handler.py +440 -0
  10. workspace_mcp-1.1.9/auth/oauth21/http.py +270 -0
  11. workspace_mcp-1.1.9/auth/oauth21/jwt.py +438 -0
  12. workspace_mcp-1.1.9/auth/oauth21/middleware.py +426 -0
  13. workspace_mcp-1.1.9/auth/oauth21/oauth2.py +353 -0
  14. workspace_mcp-1.1.9/auth/oauth21/sessions.py +519 -0
  15. workspace_mcp-1.1.9/auth/oauth21/tokens.py +392 -0
  16. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/auth/oauth_callback_server.py +1 -1
  17. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/auth/service_decorator.py +2 -5
  18. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/core/comments.py +0 -3
  19. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/core/server.py +35 -36
  20. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/core/utils.py +3 -4
  21. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/gTasks/tasks_tools.py +1 -2
  22. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/gcalendar/calendar_tools.py +4 -5
  23. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/gchat/chat_tools.py +0 -1
  24. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/gdocs/docs_tools.py +73 -16
  25. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/gdrive/drive_tools.py +1 -3
  26. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/gforms/forms_tools.py +0 -1
  27. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/gmail/gmail_tools.py +184 -70
  28. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/gsheets/sheets_tools.py +0 -2
  29. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/gslides/slides_tools.py +1 -3
  30. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/main.py +2 -2
  31. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/pyproject.toml +2 -1
  32. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/tests/test_auth.py +1 -2
  33. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/tests/test_oauth_callback_server.py +0 -2
  34. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/workspace_mcp.egg-info/PKG-INFO +3 -2
  35. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/workspace_mcp.egg-info/SOURCES.txt +12 -0
  36. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/workspace_mcp.egg-info/requires.txt +1 -0
  37. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/LICENSE +0 -0
  38. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/auth/__init__.py +0 -0
  39. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/auth/oauth_responses.py +0 -0
  40. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/auth/scopes.py +0 -0
  41. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/core/__init__.py +0 -0
  42. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/core/context.py +0 -0
  43. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/gTasks/__init__.py +0 -0
  44. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/gcalendar/__init__.py +0 -0
  45. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/gchat/__init__.py +0 -0
  46. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/gdocs/__init__.py +0 -0
  47. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/gdrive/__init__.py +0 -0
  48. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/gforms/__init__.py +0 -0
  49. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/gmail/__init__.py +0 -0
  50. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/gsheets/__init__.py +0 -0
  51. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/gslides/__init__.py +0 -0
  52. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/setup.cfg +0 -0
  53. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/workspace_mcp.egg-info/dependency_links.txt +0 -0
  54. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/workspace_mcp.egg-info/entry_points.txt +0 -0
  55. {workspace_mcp-1.1.7 → workspace_mcp-1.1.9}/workspace_mcp.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: workspace-mcp
3
- Version: 1.1.7
3
+ Version: 1.1.9
4
4
  Summary: Comprehensive, highly performant Google Workspace Streamable HTTP & SSE MCP Server for Calendar, Gmail, Docs, Sheets, Slides & Drive
5
5
  Author-email: Taylor Wilsdon <taylor@taylorwilsdon.com>
6
6
  License: MIT
@@ -37,6 +37,7 @@ Requires-Dist: google-auth-httplib2>=0.2.0
37
37
  Requires-Dist: google-auth-oauthlib>=1.2.2
38
38
  Requires-Dist: httpx>=0.28.1
39
39
  Requires-Dist: pyjwt>=2.10.1
40
+ Requires-Dist: ruff>=0.12.4
40
41
  Requires-Dist: tomlkit
41
42
  Dynamic: license-file
42
43
 
@@ -118,7 +119,7 @@ A production-ready MCP server that integrates all major Google Workspace service
118
119
  1. **Download:** Grab the latest `google_workspace_mcp.dxt` from the “Releases” page
119
120
  2. **Install:** Double-click the file – Claude Desktop opens and prompts you to **Install**
120
121
  3. **Configure:** In Claude Desktop → **Settings → Extensions → Google Workspace MCP**, paste your Google OAuth credentials
121
- 4. **Use it:** Start a new Claude chat and call any Google Workspace tool 🎉
122
+ 4. **Use it:** Start a new Claude chat and call any Google Workspace tool
122
123
 
123
124
  >
124
125
  **Why DXT?**
@@ -76,7 +76,7 @@ A production-ready MCP server that integrates all major Google Workspace service
76
76
  1. **Download:** Grab the latest `google_workspace_mcp.dxt` from the “Releases” page
77
77
  2. **Install:** Double-click the file – Claude Desktop opens and prompts you to **Install**
78
78
  3. **Configure:** In Claude Desktop → **Settings → Extensions → Google Workspace MCP**, paste your Google OAuth credentials
79
- 4. **Use it:** Start a new Claude chat and call any Google Workspace tool 🎉
79
+ 4. **Use it:** Start a new Claude chat and call any Google Workspace tool
80
80
 
81
81
  >
82
82
  **Why DXT?**
@@ -555,7 +555,7 @@ def get_credentials(
555
555
  # Check for single-user mode
556
556
  if os.getenv("MCP_SINGLE_USER_MODE") == "1":
557
557
  logger.info(
558
- f"[get_credentials] Single-user mode: bypassing session mapping, finding any credentials"
558
+ "[get_credentials] Single-user mode: bypassing session mapping, finding any credentials"
559
559
  )
560
560
  credentials = _find_any_credentials(credentials_base_dir)
561
561
  if not credentials:
@@ -0,0 +1,108 @@
1
+ """
2
+ OAuth 2.1 Authentication for MCP Server
3
+
4
+ A comprehensive OAuth 2.1 implementation with Bearer token support, session management,
5
+ and multi-user capabilities for Model Context Protocol servers.
6
+
7
+ Key Features:
8
+ - OAuth 2.1 compliant authorization flow with PKCE
9
+ - Bearer token authentication (JWT and opaque tokens)
10
+ - Multi-user session management with proper isolation
11
+ - Authorization server discovery (RFC9728 & RFC8414)
12
+ - Backward compatibility with existing authentication
13
+
14
+ Usage:
15
+ # Basic setup
16
+ from auth.oauth21 import create_auth_config, AuthCompatibilityLayer
17
+
18
+ config = create_auth_config()
19
+ auth_layer = AuthCompatibilityLayer(config)
20
+
21
+ # Initialize
22
+ await auth_layer.start()
23
+
24
+ # Use with FastAPI
25
+ middleware = auth_layer.create_enhanced_middleware()
26
+ app.add_middleware(type(middleware), **middleware.__dict__)
27
+ """
28
+
29
+ from .config import (
30
+ OAuth2Config,
31
+ AuthConfig,
32
+ create_auth_config,
33
+ create_default_oauth2_config,
34
+ load_config_from_file,
35
+ get_config_summary,
36
+ )
37
+
38
+ from .handler import OAuth2Handler
39
+
40
+ from .compat import (
41
+ AuthCompatibilityLayer,
42
+ create_compatible_auth_handler,
43
+ get_enhanced_credentials,
44
+ )
45
+
46
+ from .middleware import (
47
+ AuthenticationMiddleware,
48
+ AuthContext,
49
+ get_auth_context,
50
+ require_auth,
51
+ require_scopes,
52
+ )
53
+
54
+ from .sessions import Session, SessionStore
55
+
56
+ from .tokens import TokenValidator, TokenValidationError
57
+
58
+ from .example_config import (
59
+ create_google_workspace_oauth2_config,
60
+ create_development_config,
61
+ create_production_config,
62
+ create_single_user_config,
63
+ create_hybrid_config,
64
+ example_environment_variables,
65
+ )
66
+
67
+ __version__ = "1.0.0"
68
+
69
+ __all__ = [
70
+ # Configuration
71
+ "OAuth2Config",
72
+ "AuthConfig",
73
+ "create_auth_config",
74
+ "create_default_oauth2_config",
75
+ "load_config_from_file",
76
+ "get_config_summary",
77
+
78
+ # Main handlers
79
+ "OAuth2Handler",
80
+ "AuthCompatibilityLayer",
81
+ "create_compatible_auth_handler",
82
+
83
+ # Middleware and context
84
+ "AuthenticationMiddleware",
85
+ "AuthContext",
86
+ "get_auth_context",
87
+ "require_auth",
88
+ "require_scopes",
89
+
90
+ # Session management
91
+ "Session",
92
+ "SessionStore",
93
+
94
+ # Token handling
95
+ "TokenValidator",
96
+ "TokenValidationError",
97
+
98
+ # Enhanced credential function
99
+ "get_enhanced_credentials",
100
+
101
+ # Example configurations
102
+ "create_google_workspace_oauth2_config",
103
+ "create_development_config",
104
+ "create_production_config",
105
+ "create_single_user_config",
106
+ "create_hybrid_config",
107
+ "example_environment_variables",
108
+ ]
@@ -0,0 +1,422 @@
1
+ """
2
+ Backward Compatibility Layer
3
+
4
+ Maintains compatibility with existing authentication methods while providing
5
+ access to OAuth 2.1 features. Bridges legacy and modern authentication.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ from typing import Dict, Any, Optional, List
11
+ from datetime import datetime, timezone
12
+
13
+ from google.oauth2.credentials import Credentials
14
+
15
+ from .config import AuthConfig
16
+ from .handler import OAuth2Handler
17
+ from .middleware import AuthContext
18
+ from ..google_auth import (
19
+ get_credentials as legacy_get_credentials,
20
+ )
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class AuthCompatibilityLayer:
26
+ """Maintains compatibility with existing auth methods."""
27
+
28
+ def __init__(self, auth_config: AuthConfig):
29
+ """
30
+ Initialize compatibility layer.
31
+
32
+ Args:
33
+ auth_config: Complete authentication configuration
34
+ """
35
+ self.config = auth_config
36
+ self.oauth2_handler: Optional[OAuth2Handler] = None
37
+
38
+ # Initialize OAuth 2.1 handler if enabled
39
+ if self.config.is_oauth2_enabled():
40
+ self.oauth2_handler = OAuth2Handler(self.config.oauth2)
41
+
42
+ async def start(self):
43
+ """Start the compatibility layer."""
44
+ if self.oauth2_handler:
45
+ await self.oauth2_handler.start()
46
+ logger.info("Authentication compatibility layer started")
47
+
48
+ async def stop(self):
49
+ """Stop the compatibility layer."""
50
+ if self.oauth2_handler:
51
+ await self.oauth2_handler.stop()
52
+ logger.info("Authentication compatibility layer stopped")
53
+
54
+ def supports_legacy_auth(self, request: Optional[Any] = None) -> bool:
55
+ """
56
+ Check if request uses legacy authentication.
57
+
58
+ Args:
59
+ request: HTTP request (optional)
60
+
61
+ Returns:
62
+ True if legacy authentication should be used
63
+ """
64
+ # Always support legacy auth if enabled
65
+ if not self.config.enable_legacy_auth:
66
+ return False
67
+
68
+ # In single user mode, prefer legacy
69
+ if self.config.single_user_mode:
70
+ return True
71
+
72
+ # If OAuth 2.1 is not configured, use legacy
73
+ if not self.config.is_oauth2_enabled():
74
+ return True
75
+
76
+ # Check if request has Bearer token (suggesting OAuth 2.1)
77
+ if request and hasattr(request, 'headers'):
78
+ auth_header = request.headers.get('authorization', '')
79
+ if auth_header.lower().startswith('bearer '):
80
+ return False
81
+
82
+ # Default to supporting legacy for backward compatibility
83
+ return True
84
+
85
+ async def handle_legacy_auth(
86
+ self,
87
+ user_google_email: Optional[str],
88
+ required_scopes: List[str],
89
+ session_id: Optional[str] = None,
90
+ client_secrets_path: Optional[str] = None,
91
+ credentials_base_dir: Optional[str] = None,
92
+ ) -> Optional[Credentials]:
93
+ """
94
+ Process legacy authentication.
95
+
96
+ Args:
97
+ user_google_email: User's Google email
98
+ required_scopes: Required OAuth scopes
99
+ session_id: Session identifier
100
+ client_secrets_path: Path to client secrets file
101
+ credentials_base_dir: Base directory for credentials
102
+
103
+ Returns:
104
+ Google credentials or None if authentication fails
105
+ """
106
+ try:
107
+ credentials = await asyncio.to_thread(
108
+ legacy_get_credentials,
109
+ user_google_email=user_google_email,
110
+ required_scopes=required_scopes,
111
+ client_secrets_path=client_secrets_path,
112
+ credentials_base_dir=credentials_base_dir or self.config.legacy_credentials_dir,
113
+ session_id=session_id,
114
+ )
115
+
116
+ if credentials:
117
+ logger.debug(f"Legacy authentication successful for {user_google_email}")
118
+
119
+ # Bridge to OAuth 2.1 session if available
120
+ if self.oauth2_handler and session_id:
121
+ await self._bridge_legacy_to_oauth2(
122
+ credentials, user_google_email, session_id, required_scopes
123
+ )
124
+
125
+ return credentials
126
+
127
+ except Exception as e:
128
+ logger.error(f"Legacy authentication failed: {e}")
129
+ return None
130
+
131
+ async def get_unified_credentials(
132
+ self,
133
+ user_google_email: Optional[str],
134
+ required_scopes: List[str],
135
+ session_id: Optional[str] = None,
136
+ request: Optional[Any] = None,
137
+ prefer_oauth2: bool = False,
138
+ ) -> Optional[Credentials]:
139
+ """
140
+ Get credentials using unified authentication approach.
141
+
142
+ Args:
143
+ user_google_email: User's Google email
144
+ required_scopes: Required OAuth scopes
145
+ session_id: Session identifier
146
+ request: HTTP request object
147
+ prefer_oauth2: Whether to prefer OAuth 2.1 over legacy
148
+
149
+ Returns:
150
+ Google credentials or None
151
+ """
152
+ # Determine authentication method
153
+ use_oauth2 = (
154
+ self.config.is_oauth2_enabled() and
155
+ (prefer_oauth2 or not self.supports_legacy_auth(request))
156
+ )
157
+
158
+ if use_oauth2:
159
+ # Try OAuth 2.1 authentication first
160
+ credentials = await self._get_oauth2_credentials(
161
+ user_google_email, required_scopes, session_id, request
162
+ )
163
+
164
+ # Fallback to legacy if OAuth 2.1 fails and legacy is enabled
165
+ if not credentials and self.config.enable_legacy_auth:
166
+ logger.debug("OAuth 2.1 authentication failed, falling back to legacy")
167
+ credentials = await self.handle_legacy_auth(
168
+ user_google_email, required_scopes, session_id
169
+ )
170
+ else:
171
+ # Use legacy authentication
172
+ credentials = await self.handle_legacy_auth(
173
+ user_google_email, required_scopes, session_id
174
+ )
175
+
176
+ return credentials
177
+
178
+ async def _get_oauth2_credentials(
179
+ self,
180
+ user_google_email: Optional[str],
181
+ required_scopes: List[str],
182
+ session_id: Optional[str],
183
+ request: Optional[Any],
184
+ ) -> Optional[Credentials]:
185
+ """Get credentials using OAuth 2.1."""
186
+ if not self.oauth2_handler:
187
+ return None
188
+
189
+ try:
190
+ # Extract Bearer token from request if available
191
+ bearer_token = None
192
+ if request and hasattr(request, 'headers'):
193
+ auth_header = request.headers.get('authorization', '')
194
+ if auth_header.lower().startswith('bearer '):
195
+ bearer_token = auth_header[7:] # Remove 'Bearer ' prefix
196
+
197
+ # Try session-based authentication first
198
+ if session_id:
199
+ session_info = self.oauth2_handler.get_session_info(session_id)
200
+ if session_info:
201
+ return self._convert_oauth2_to_credentials(session_info)
202
+
203
+ # Try Bearer token authentication
204
+ if bearer_token:
205
+ auth_context = await self.oauth2_handler.authenticate_bearer_token(
206
+ token=bearer_token,
207
+ required_scopes=required_scopes,
208
+ create_session=bool(session_id),
209
+ )
210
+
211
+ if auth_context.authenticated:
212
+ return self._convert_oauth2_to_credentials(auth_context.token_info)
213
+
214
+ return None
215
+
216
+ except Exception as e:
217
+ logger.error(f"OAuth 2.1 credential retrieval failed: {e}")
218
+ return None
219
+
220
+ def _convert_oauth2_to_credentials(self, token_info: Dict[str, Any]) -> Optional[Credentials]:
221
+ """Convert OAuth 2.1 token info to Google Credentials."""
222
+ try:
223
+ # Extract token information
224
+ access_token = token_info.get("access_token")
225
+ refresh_token = token_info.get("refresh_token")
226
+ token_uri = token_info.get("token_uri") or "https://oauth2.googleapis.com/token"
227
+ client_id = token_info.get("client_id") or self.config.oauth2.client_id
228
+ client_secret = token_info.get("client_secret") or self.config.oauth2.client_secret
229
+ scopes = token_info.get("scopes", [])
230
+
231
+ if not access_token:
232
+ return None
233
+
234
+ # Parse expiry
235
+ expiry = None
236
+ if "expires_at" in token_info:
237
+ exp_timestamp = token_info["expires_at"]
238
+ if isinstance(exp_timestamp, (int, float)):
239
+ expiry = datetime.fromtimestamp(exp_timestamp, tz=timezone.utc)
240
+
241
+ # Create Google Credentials object
242
+ credentials = Credentials(
243
+ token=access_token,
244
+ refresh_token=refresh_token,
245
+ token_uri=token_uri,
246
+ client_id=client_id,
247
+ client_secret=client_secret,
248
+ scopes=scopes,
249
+ expiry=expiry,
250
+ )
251
+
252
+ logger.debug("Successfully converted OAuth 2.1 token to Google Credentials")
253
+ return credentials
254
+
255
+ except Exception as e:
256
+ logger.error(f"Failed to convert OAuth 2.1 token to credentials: {e}")
257
+ return None
258
+
259
+ async def _bridge_legacy_to_oauth2(
260
+ self,
261
+ credentials: Credentials,
262
+ user_email: str,
263
+ session_id: str,
264
+ scopes: List[str],
265
+ ):
266
+ """Bridge legacy credentials to OAuth 2.1 session."""
267
+ if not self.oauth2_handler:
268
+ return
269
+
270
+ try:
271
+ # Convert legacy credentials to OAuth 2.1 token format
272
+ token_info = {
273
+ "access_token": credentials.token,
274
+ "refresh_token": credentials.refresh_token,
275
+ "token_uri": credentials.token_uri,
276
+ "client_id": credentials.client_id,
277
+ "client_secret": credentials.client_secret,
278
+ "scopes": credentials.scopes or scopes,
279
+ "expires_at": credentials.expiry.timestamp() if credentials.expiry else None,
280
+ "token_type": "Bearer",
281
+ }
282
+
283
+ # Create OAuth 2.1 session
284
+ oauth2_session_id = self.oauth2_handler.session_store.create_session(
285
+ user_id=user_email,
286
+ token_info=token_info,
287
+ scopes=scopes,
288
+ metadata={
289
+ "bridged_from": "legacy_auth",
290
+ "legacy_session_id": session_id,
291
+ }
292
+ )
293
+
294
+ logger.debug(f"Bridged legacy credentials to OAuth 2.1 session {oauth2_session_id}")
295
+
296
+ except Exception as e:
297
+ logger.error(f"Failed to bridge legacy credentials to OAuth 2.1: {e}")
298
+
299
+ def create_enhanced_middleware(self):
300
+ """Create middleware that supports both OAuth 2.1 and legacy auth."""
301
+ if not self.oauth2_handler:
302
+ return None
303
+
304
+ # Get base OAuth 2.1 middleware
305
+ middleware = self.oauth2_handler.create_middleware()
306
+
307
+ # Enhance it with legacy support
308
+ original_authenticate = middleware.authenticate_request
309
+
310
+ async def enhanced_authenticate(request):
311
+ """Enhanced authentication that supports legacy fallback."""
312
+ # Try OAuth 2.1 first
313
+ auth_context = await original_authenticate(request)
314
+
315
+ # If OAuth 2.1 fails and legacy is supported, try legacy
316
+ if (not auth_context.authenticated and
317
+ self.supports_legacy_auth(request) and
318
+ self.config.enable_legacy_auth):
319
+
320
+ # Extract session ID for legacy auth
321
+ session_id = middleware._extract_session_id(request)
322
+
323
+ # Try to get user email (this is a limitation of legacy auth)
324
+ user_email = self.config.default_user_email
325
+ if not user_email:
326
+ # Could extract from request parameters or headers
327
+ user_email = request.query_params.get('user_google_email')
328
+
329
+ if user_email:
330
+ try:
331
+ credentials = await self.handle_legacy_auth(
332
+ user_google_email=user_email,
333
+ required_scopes=self.config.oauth2.required_scopes,
334
+ session_id=session_id,
335
+ )
336
+
337
+ if credentials:
338
+ # Create auth context from legacy credentials
339
+ auth_context = AuthContext(
340
+ authenticated=True,
341
+ user_id=user_email,
342
+ session_id=session_id,
343
+ token_info={
344
+ "access_token": credentials.token,
345
+ "scopes": credentials.scopes or [],
346
+ "auth_method": "legacy",
347
+ },
348
+ scopes=credentials.scopes or [],
349
+ )
350
+ except Exception as e:
351
+ logger.error(f"Legacy auth fallback failed: {e}")
352
+
353
+ return auth_context
354
+
355
+ # Replace the authenticate method
356
+ middleware.authenticate_request = enhanced_authenticate
357
+ return middleware
358
+
359
+ def get_auth_mode_info(self) -> Dict[str, Any]:
360
+ """Get information about current authentication mode."""
361
+ return {
362
+ "mode": self.config.get_effective_auth_mode(),
363
+ "oauth2_enabled": self.config.is_oauth2_enabled(),
364
+ "legacy_enabled": self.config.enable_legacy_auth,
365
+ "single_user_mode": self.config.single_user_mode,
366
+ "default_user_email": self.config.default_user_email,
367
+ "oauth2_config": self.config.oauth2.to_dict() if self.config.oauth2 else None,
368
+ }
369
+
370
+ async def __aenter__(self):
371
+ """Async context manager entry."""
372
+ await self.start()
373
+ return self
374
+
375
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
376
+ """Async context manager exit."""
377
+ await self.stop()
378
+
379
+
380
+ # Legacy compatibility functions
381
+ async def get_enhanced_credentials(
382
+ user_google_email: Optional[str],
383
+ required_scopes: List[str],
384
+ session_id: Optional[str] = None,
385
+ request: Optional[Any] = None,
386
+ auth_config: Optional[AuthConfig] = None,
387
+ **kwargs
388
+ ) -> Optional[Credentials]:
389
+ """
390
+ Enhanced version of get_credentials that supports OAuth 2.1.
391
+
392
+ This function maintains backward compatibility while adding OAuth 2.1 support.
393
+ """
394
+ if not auth_config:
395
+ # Create default config that tries to enable OAuth 2.1
396
+ auth_config = AuthConfig()
397
+
398
+ compat_layer = AuthCompatibilityLayer(auth_config)
399
+
400
+ async with compat_layer:
401
+ return await compat_layer.get_unified_credentials(
402
+ user_google_email=user_google_email,
403
+ required_scopes=required_scopes,
404
+ session_id=session_id,
405
+ request=request,
406
+ )
407
+
408
+
409
+ def create_compatible_auth_handler(auth_config: Optional[AuthConfig] = None) -> AuthCompatibilityLayer:
410
+ """
411
+ Create a compatible authentication handler.
412
+
413
+ Args:
414
+ auth_config: Authentication configuration
415
+
416
+ Returns:
417
+ Authentication compatibility layer
418
+ """
419
+ if not auth_config:
420
+ auth_config = AuthConfig()
421
+
422
+ return AuthCompatibilityLayer(auth_config)