O2Sound_ver2_final/backend/app/infra/google/oauth.py

134 lines
5.2 KiB
Python

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