""" Routes for Google authentication. """ from fastapi import APIRouter, HTTPException, Request, Response, status, Depends, Body, Query from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse import logging import json from typing import Dict, Any, Optional from datetime import datetime, timedelta from requests_oauthlib import OAuth2Session import httpx from modules.shared.configuration import APP_CONFIG from modules.interfaces.interfaceAppObjects import getInterface, getRootInterface from modules.interfaces.interfaceAppModel import AuthAuthority, User, Token, ConnectionStatus, UserConnection from modules.security.auth import getCurrentUser, limiter from modules.shared.attributeUtils import ModelMixin from modules.shared.timezoneUtils import get_utc_now, create_expiration_timestamp, get_utc_timestamp # Configure logger logger = logging.getLogger(__name__) # Create router router = APIRouter( prefix="/api/google", tags=["Security Google"], responses={ 404: {"description": "Not found"}, 400: {"description": "Bad request"}, 401: {"description": "Unauthorized"}, 403: {"description": "Forbidden"}, 500: {"description": "Internal server error"} } ) # Google OAuth configuration CLIENT_ID = APP_CONFIG.get("Service_GOOGLE_CLIENT_ID") CLIENT_SECRET = APP_CONFIG.get("Service_GOOGLE_CLIENT_SECRET") REDIRECT_URI = APP_CONFIG.get("Service_GOOGLE_REDIRECT_URI") SCOPES = [ "https://www.googleapis.com/auth/gmail.readonly", "https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.email", "openid" ] @router.get("/config") async def get_config(): """Debug endpoint to check Google OAuth configuration""" return { "client_id": CLIENT_ID, "client_secret": "***" if CLIENT_SECRET else None, "redirect_uri": REDIRECT_URI, "scopes": SCOPES, "config_loaded": bool(CLIENT_ID and CLIENT_SECRET and REDIRECT_URI), "config_source": { "client_id_from": "config.ini" if CLIENT_ID and "354925410565" in CLIENT_ID else "env file", "redirect_uri_from": "config.ini" if REDIRECT_URI and "gateway-int.poweron-center.net" in REDIRECT_URI else "env file" } } @router.get("/login") @limiter.limit("5/minute") async def login( request: Request, state: str = Query("login", description="State parameter to distinguish between login and connection flows"), connectionId: Optional[str] = Query(None, description="Connection ID for connection flow") ) -> RedirectResponse: """Initiate Google login""" try: # Debug: Log configuration values logger.info(f"Google OAuth Configuration - CLIENT_ID: {CLIENT_ID}, REDIRECT_URI: {REDIRECT_URI}") # Validate required configuration if not CLIENT_ID: logger.error("Google OAuth CLIENT_ID is not configured") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Google OAuth CLIENT_ID is not configured" ) if not CLIENT_SECRET: logger.error("Google OAuth CLIENT_SECRET is not configured") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Google OAuth CLIENT_SECRET is not configured" ) if not REDIRECT_URI: logger.error("Google OAuth REDIRECT_URI is not configured") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Google OAuth REDIRECT_URI is not configured" ) # Generate auth URL with state - use state as is if it's already JSON, otherwise create new state try: # Try to parse state as JSON to check if it's already encoded json.loads(state) state_param = state # Use state as is if it's valid JSON except json.JSONDecodeError: # If not JSON, create new state object state_param = json.dumps({ "type": state, "connectionId": connectionId }) logger.info(f"Using state parameter: {state_param}") # Use OAuth2Session directly - it works reliably oauth = OAuth2Session( client_id=CLIENT_ID, redirect_uri=REDIRECT_URI, scope=SCOPES ) auth_url, state = oauth.authorization_url( "https://accounts.google.com/o/oauth2/auth", access_type="offline", include_granted_scopes="true", state=state_param, prompt="select_account" ) logger.info(f"Generated Google OAuth URL using OAuth2Session: {auth_url}") return RedirectResponse(auth_url) except Exception as e: logger.error(f"Error initiating Google login: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to initiate Google login: {str(e)}" ) @router.get("/auth/callback") async def auth_callback(code: str, state: str, request: Request) -> HTMLResponse: """Handle Google OAuth callback""" try: # Parse state state_data = json.loads(state) state_type = state_data.get("type", "login") connection_id = state_data.get("connectionId") user_id = state_data.get("userId") # Get user ID from state logger.info(f"Processing Google auth callback: state_type={state_type}, connection_id={connection_id}, user_id={user_id}") # Use OAuth2Session directly for token exchange oauth = OAuth2Session( client_id=CLIENT_ID, redirect_uri=REDIRECT_URI ) # Get token using OAuth2Session token_data = oauth.fetch_token( "https://oauth2.googleapis.com/token", client_secret=CLIENT_SECRET, code=code, include_client_id=True ) token_response = { "access_token": token_data.get("access_token"), "refresh_token": token_data.get("refresh_token", ""), "token_type": token_data.get("token_type", "bearer"), "expires_in": token_data.get("expires_in", 0) } if not token_response.get("access_token"): logger.error("Token acquisition failed: No access token received") return HTMLResponse( content="
Could not acquire token.
", status_code=400 ) # Get user info using the access token headers = { 'Authorization': f"Bearer {token_response['access_token']}", 'Content-Type': 'application/json' } async with httpx.AsyncClient() as client: user_info_response = await client.get( "https://www.googleapis.com/oauth2/v2/userinfo", headers=headers ) if user_info_response.status_code != 200: logger.error(f"Failed to get user info: {user_info_response.text}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to get user info from Google" ) user_info = user_info_response.json() logger.info(f"Got user info from Google: {user_info.get('email')}") if state_type == "login": # Handle login flow rootInterface = getRootInterface() user = rootInterface.getUserByUsername(user_info.get("email")) if not user: # Create new user if doesn't exist user = rootInterface.createUser( username=user_info.get("email"), email=user_info.get("email"), fullName=user_info.get("name"), authenticationAuthority=AuthAuthority.GOOGLE, externalId=user_info.get("id"), externalUsername=user_info.get("email"), externalEmail=user_info.get("email") ) # Create token token = Token( userId=user.id, # Use local user's ID authority=AuthAuthority.GOOGLE, tokenAccess=token_response["access_token"], tokenRefresh=token_response.get("refresh_token", ""), tokenType=token_response.get("token_type", "bearer"), expiresAt=create_expiration_timestamp(token_response.get("expires_in", 0)), createdAt=get_utc_timestamp() ) # Save access token (no connectionId) appInterface = getInterface(user) appInterface.saveAccessToken(token) # Return success page with token data return HTMLResponse( content=f"""