workspace-mcp 1.1.8__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.
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/PKG-INFO +3 -2
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/README.md +1 -1
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/auth/google_auth.py +1 -1
- workspace_mcp-1.1.9/auth/oauth21/__init__.py +108 -0
- workspace_mcp-1.1.9/auth/oauth21/compat.py +422 -0
- workspace_mcp-1.1.9/auth/oauth21/config.py +380 -0
- workspace_mcp-1.1.9/auth/oauth21/discovery.py +232 -0
- workspace_mcp-1.1.9/auth/oauth21/example_config.py +303 -0
- workspace_mcp-1.1.9/auth/oauth21/handler.py +440 -0
- workspace_mcp-1.1.9/auth/oauth21/http.py +270 -0
- workspace_mcp-1.1.9/auth/oauth21/jwt.py +438 -0
- workspace_mcp-1.1.9/auth/oauth21/middleware.py +426 -0
- workspace_mcp-1.1.9/auth/oauth21/oauth2.py +353 -0
- workspace_mcp-1.1.9/auth/oauth21/sessions.py +519 -0
- workspace_mcp-1.1.9/auth/oauth21/tokens.py +392 -0
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/auth/oauth_callback_server.py +1 -1
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/auth/service_decorator.py +2 -5
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/core/comments.py +0 -3
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/core/server.py +35 -36
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/core/utils.py +3 -4
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/gTasks/tasks_tools.py +1 -2
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/gcalendar/calendar_tools.py +4 -5
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/gchat/chat_tools.py +0 -1
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/gdocs/docs_tools.py +73 -16
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/gdrive/drive_tools.py +1 -3
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/gforms/forms_tools.py +0 -1
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/gmail/gmail_tools.py +184 -70
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/gsheets/sheets_tools.py +0 -2
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/gslides/slides_tools.py +1 -3
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/main.py +2 -2
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/pyproject.toml +2 -1
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/tests/test_auth.py +1 -2
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/tests/test_oauth_callback_server.py +0 -2
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/workspace_mcp.egg-info/PKG-INFO +3 -2
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/workspace_mcp.egg-info/SOURCES.txt +12 -0
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/workspace_mcp.egg-info/requires.txt +1 -0
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/LICENSE +0 -0
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/auth/__init__.py +0 -0
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/auth/oauth_responses.py +0 -0
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/auth/scopes.py +0 -0
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/core/__init__.py +0 -0
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/core/context.py +0 -0
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/gTasks/__init__.py +0 -0
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/gcalendar/__init__.py +0 -0
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/gchat/__init__.py +0 -0
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/gdocs/__init__.py +0 -0
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/gdrive/__init__.py +0 -0
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/gforms/__init__.py +0 -0
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/gmail/__init__.py +0 -0
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/gsheets/__init__.py +0 -0
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/gslides/__init__.py +0 -0
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/setup.cfg +0 -0
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/workspace_mcp.egg-info/dependency_links.txt +0 -0
- {workspace_mcp-1.1.8 → workspace_mcp-1.1.9}/workspace_mcp.egg-info/entry_points.txt +0 -0
- {workspace_mcp-1.1.8 → 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.
|
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
|
-
|
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)
|