From 85b93c84eded6021b5cd40a14d80f91ecdc2c598 Mon Sep 17 00:00:00 2001 From: Olivier Gintrand Date: Tue, 14 Apr 2026 16:55:46 +0200 Subject: [PATCH] fix(oauth): handle missing expires_in in OAuth token response Signed-off-by: Olivier Gintrand --- mcpgateway/services/oauth_manager.py | 7 ++++++- mcpgateway/services/token_storage_service.py | 12 ++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/mcpgateway/services/oauth_manager.py b/mcpgateway/services/oauth_manager.py index 3e190aa8f5..aa3449e6f6 100644 --- a/mcpgateway/services/oauth_manager.py +++ b/mcpgateway/services/oauth_manager.py @@ -573,13 +573,18 @@ async def complete_authorization_code_flow(self, gateway_id: str, code: str, sta # Store tokens if storage service is available if self.token_storage: + # Use provider's expires_in if present; None means the provider + # does not specify token expiration (e.g. GitHub OAuth Apps). + raw_expires = token_response.get("expires_in") + expires_in = int(raw_expires) if raw_expires is not None else None + token_record = await self.token_storage.store_tokens( gateway_id=gateway_id, user_id=user_id, app_user_email=app_user_email, # User from state access_token=token_response["access_token"], refresh_token=token_response.get("refresh_token"), - expires_in=token_response.get("expires_in", self.settings.oauth_default_timeout), + expires_in=expires_in, scopes=token_response.get("scope", "").split(), ) diff --git a/mcpgateway/services/token_storage_service.py b/mcpgateway/services/token_storage_service.py index 4257e17404..1c4c18954e 100644 --- a/mcpgateway/services/token_storage_service.py +++ b/mcpgateway/services/token_storage_service.py @@ -74,7 +74,7 @@ def __init__(self, db: Session): logger.warning("OAuth encryption not available, using plain text storage") self.encryption = None - async def store_tokens(self, gateway_id: str, user_id: str, app_user_email: str, access_token: str, refresh_token: Optional[str], expires_in: int, scopes: List[str]) -> OAuthToken: + async def store_tokens(self, gateway_id: str, user_id: str, app_user_email: str, access_token: str, refresh_token: Optional[str], expires_in: Optional[int], scopes: List[str]) -> OAuthToken: """Store OAuth tokens for a gateway-user combination. Args: @@ -83,7 +83,7 @@ async def store_tokens(self, gateway_id: str, user_id: str, app_user_email: str, app_user_email: ContextForge user email (required) access_token: Access token from OAuth provider refresh_token: Refresh token from OAuth provider (optional) - expires_in: Token expiration time in seconds + expires_in: Token expiration time in seconds, or None if the provider does not specify expiration scopes: List of OAuth scopes granted Returns: @@ -102,8 +102,12 @@ async def store_tokens(self, gateway_id: str, user_id: str, app_user_email: str, if refresh_token: encrypted_refresh = await self.encryption.encrypt_secret_async(refresh_token) - # Calculate expiration - expires_at = datetime.now(timezone.utc) + timedelta(seconds=int(expires_in)) + # Calculate expiration (None if provider does not specify expires_in) + if expires_in is not None: + expires_at = datetime.now(timezone.utc) + timedelta(seconds=int(expires_in)) + else: + logger.info(f"No expires_in from OAuth provider for gateway {SecurityValidator.sanitize_log_message(gateway_id)} — token will not auto-expire") + expires_at = None # Create or update token record - now scoped by app_user_email token_record = self.db.execute(select(OAuthToken).where(OAuthToken.gateway_id == gateway_id, OAuthToken.app_user_email == app_user_email)).scalar_one_or_none()