diff --git a/frontend/app/api/mutations/useConnectConnectorMutation.ts b/frontend/app/api/mutations/useConnectConnectorMutation.ts index 893a90086..afb0cc46a 100644 --- a/frontend/app/api/mutations/useConnectConnectorMutation.ts +++ b/frontend/app/api/mutations/useConnectConnectorMutation.ts @@ -77,7 +77,8 @@ export const useConnectConnectorMutation = () => { result.oauth_config.redirect_uri, )}&` + `access_type=offline&` + - `prompt=select_account&` + + `include_granted_scopes=true&` + + `prompt=consent&` + `state=${result.connection_id}`; window.location.href = authUrl; diff --git a/frontend/contexts/auth-context.tsx b/frontend/contexts/auth-context.tsx index 004045314..4552e6eb9 100644 --- a/frontend/contexts/auth-context.tsx +++ b/frontend/contexts/auth-context.tsx @@ -132,7 +132,8 @@ export function AuthProvider({ children }: AuthProviderProps) { `scope=${result.oauth_config.scopes.join(" ")}&` + `redirect_uri=${encodeURIComponent(result.oauth_config.redirect_uri)}&` + `access_type=offline&` + - `prompt=select_account&` + + `include_granted_scopes=true&` + + `prompt=consent&` + `state=${result.connection_id}`; console.log("Redirecting to OAuth URL:", authUrl); diff --git a/src/connectors/google_drive/oauth.py b/src/connectors/google_drive/oauth.py index e9beb0a36..0c33102ba 100644 --- a/src/connectors/google_drive/oauth.py +++ b/src/connectors/google_drive/oauth.py @@ -1,6 +1,7 @@ import os import json from typing import Optional +from datetime import datetime, timezone from google.auth.transport.requests import Request from google.oauth2.credentials import Credentials from google_auth_oauthlib.flow import Flow @@ -50,13 +51,18 @@ async def load_credentials(self) -> Optional[Credentials]: scopes=token_data.get("scopes", self.SCOPES), ) - # Set expiry if available (ensure timezone-naive for Google auth compatibility) + # Set expiry if available. + # Backward compatibility: older tokens were saved as naive local datetimes. if token_data.get("expiry"): - from datetime import datetime - expiry_dt = datetime.fromisoformat(token_data["expiry"]) - # Remove timezone info to make it naive (Google auth expects naive datetimes) - self.creds.expiry = expiry_dt.replace(tzinfo=None) + if expiry_dt.tzinfo is None: + local_tz = datetime.now().astimezone().tzinfo + expiry_dt = expiry_dt.replace(tzinfo=local_tz) + + # google-auth compares against a naive UTC timestamp internally. + self.creds.expiry = ( + expiry_dt.astimezone(timezone.utc).replace(tzinfo=None) + ) # If credentials are expired, refresh them if self.creds and self.creds.expired and self.creds.refresh_token: diff --git a/src/services/auth_service.py b/src/services/auth_service.py index d2fe43971..77e135e1b 100644 --- a/src/services/auth_service.py +++ b/src/services/auth_service.py @@ -4,7 +4,7 @@ import httpx import aiofiles import logging -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Optional import asyncio @@ -92,7 +92,6 @@ async def init_oauth( ) # Get OAuth configuration from connector and OAuth classes - import os # Map connector types to their connector and OAuth classes connector_class_map = { @@ -236,21 +235,36 @@ async def handle_oauth_callback( else granted_scopes ) + refresh_token = token_data.get("refresh_token") + token_file_path = connection_config.config["token_file"] + + # Some OAuth providers omit refresh_token on re-consent. Preserve an + # existing one so background token refresh keeps working. + if not refresh_token and os.path.exists(token_file_path): + try: + async with aiofiles.open(token_file_path, "r") as f: + existing = json.loads(await f.read()) + refresh_token = existing.get("refresh_token") + except Exception: + refresh_token = None + token_file_data = { "token": token_data["access_token"], - "refresh_token": token_data.get("refresh_token"), + "refresh_token": refresh_token, "scopes": scopes, } + if token_data.get("id_token"): + token_file_data["id_token"] = token_data["id_token"] + # Add expiry if provided if token_data.get("expires_in"): - expiry = datetime.now() + timedelta( + expiry = datetime.now(timezone.utc) + timedelta( seconds=int(token_data["expires_in"]) ) token_file_data["expiry"] = expiry.isoformat() # Save tokens to file - token_file_path = connection_config.config["token_file"] async with aiofiles.open(token_file_path, "w") as f: await f.write(json.dumps(token_file_data, indent=2))