import uuid from typing import Dict, Optional from datetime import datetime, timezone from fastapi import HTTPException from google_auth_oauthlib.flow import Flow from googleapiclient.discovery import build from .utils import load_client_config from .redis import RedisOAuthStorage, get_oauth_storage class OAuthService: def __init__(self, oauth_storage: RedisOAuthStorage): self.oauth_storage = oauth_storage self.scopes = [ "https://www.googleapis.com/auth/youtube.upload", "https://www.googleapis.com/auth/youtube.readonly", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile", "openid" ] self.client_secret = "client_secret.json" self.redirect_uri = "http://localhost:8000/social/google/callback" async def login_url(self, return_url: Optional[str] = None): '''Google OAuth 로그인 URL 생성''' try: client_config = load_client_config(self.client_secret) flow = Flow.from_client_config( client_config, scopes=self.scopes, redirect_uri=self.redirect_uri ) # 고유한 state 생성 ( CSRF 공격 방지 ) state = str(uuid.uuid4()) # Redis에 flow 데이터 저장 (return_url 포함) flow_data = { "client_config": client_config, "scopes": self.scopes, "redirect_uri": self.redirect_uri, "return_url": return_url, "created_at": datetime.now(timezone.utc).isoformat() } await self.oauth_storage.store_oauth_state(state, flow_data) # 인증 URL 생성 auth_url, _ = flow.authorization_url( access_type="offline", include_granted_scopes="true", state=state, prompt='consent' ) return { "auth_url": auth_url, "state": state, "return_url": return_url, "message": "Google 로그인 URL이 생성되었습니다." } except Exception as e: print(f"Google OAuth URL 생성 오류: {e}") raise HTTPException( status_code=500, detail=f"Google OAuth URL 생성 오류: {str(e)}" ) async def callback(self, state: str, code: str): '''Google OAuth 콜백 처리''' # Redis에서 flow 데이터 조회 flow_data = await self.oauth_storage.get_oauth_state(state) if not flow_data: raise HTTPException(400, "유효하지 않은 state 토큰입니다.") try: # Flow 재생성 flow = Flow.from_client_config( flow_data["client_config"], scopes=flow_data["scopes"], redirect_uri=flow_data["redirect_uri"] ) # 인증 코드로 토큰 교환 flow.fetch_token(code=code) credentials = flow.credentials # 사용자 정보 가져오기 user_info = self._get_user_info(credentials) # 토큰 데이터 준비 token_data = { "access_token": credentials.token, "refresh_token": credentials.refresh_token, "expires_at": credentials.expiry.isoformat() if credentials.expiry else None, "scopes": credentials.scopes, "user_info": user_info, "return_url": flow_data.get("return_url") } # 임시 토큰 ID 생성하여 저장 temp_token_id = str(uuid.uuid4()) await self.oauth_storage.store_temp_token(temp_token_id, token_data) return { "message": "Google 로그인 성공", "temp_token_id": temp_token_id, "return_url": flow_data.get("return_url"), "user_info": user_info, "credentials": { "access_token": credentials.token, "refresh_token": credentials.refresh_token, "expires_at": credentials.expiry.isoformat() if credentials.expiry else None, "scopes": credentials.scopes } } except Exception as e: raise HTTPException(status_code=400, detail=f"Google 인증 처리 오류: {str(e)}") def _get_user_info(self, credentials): '''Google 사용자 정보 조회''' try: service = build('oauth2', 'v2', credentials=credentials) user_info = service.userinfo().get().execute() return user_info except Exception as e: print(f"사용자 정보 조회 실패: {e}") return {"error": "사용자 정보 조회 실패"} async def get_token_by_temp_id(self, temp_token_id: str): '''임시 토큰 ID로 실제 토큰 정보 조회''' token_data = await self.oauth_storage.get_temp_token(temp_token_id) if not token_data: raise HTTPException(400, "유효하지 않은 토큰 ID입니다.") return token_data