Coverage for src / mysingle / auth / oauth_manager.py: 0%
68 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-02 00:58 +0900
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-02 00:58 +0900
1# path: app/auth/providers.py
3import logging
4import secrets
5from typing import Optional, Union
7import httpx
8from httpx_oauth.clients.google import GoogleOAuth2
9from httpx_oauth.clients.kakao import KakaoOAuth2
10from httpx_oauth.clients.naver import NaverOAuth2
12from ..core.config import settings
13from .schemas.oauth2 import (
14 BaseOAuthToken,
15 GoogleProfile,
16 GoogleToken,
17 KakaoProfile,
18 KakaoToken,
19 NaverProfile,
20 NaverToken,
21)
23logger = logging.getLogger(__name__)
25# ---------------------------
26# OAuth2 Clients
27# ---------------------------
28google_client = GoogleOAuth2(
29 client_id=settings.GOOGLE_CLIENT_ID,
30 client_secret=settings.GOOGLE_CLIENT_SECRET,
31)
32kakao_client = KakaoOAuth2(
33 client_id=settings.KAKAO_CLIENT_ID,
34 client_secret=settings.KAKAO_CLIENT_SECRET,
35)
36naver_client = NaverOAuth2(
37 client_id=settings.NAVER_CLIENT_ID,
38 client_secret=settings.NAVER_CLIENT_SECRET,
39)
41AVAILABLE_PROVIDERS = {
42 "google": google_client,
43 "kakao": kakao_client,
44 "naver": naver_client,
45}
48class OAuthManager:
49 """
50 OAuthManager 클래스를 통해
51 - Provider별 authorize URL
52 - Callback 시 Access Token & Profile
53 - User DB 업데이트
54 를 일관되게 처리.
55 """
57 @staticmethod
58 def get_provider_client(provider: str):
59 if provider not in AVAILABLE_PROVIDERS:
60 raise ValueError(f"Unknown OAuth provider: {provider}")
61 return AVAILABLE_PROVIDERS[provider]
63 @staticmethod
64 def get_redirect_uri(provider: str) -> str:
65 return f"{settings.FRONTEND_URL}/api/auth/oauth2/{provider}/callback"
67 @staticmethod
68 async def generate_auth_url(
69 provider: str, state: Optional[str], redirect_uri: str | None = None
70 ) -> str:
71 client = OAuthManager.get_provider_client(provider)
72 if not redirect_uri:
73 redirect_uri = OAuthManager.get_redirect_uri(provider)
74 state_string = state or secrets.token_urlsafe(16)
75 scope = None
76 if provider == "google":
77 scope = ["openid", "email", "profile"]
78 try:
79 authorization_url = await client.get_authorization_url(
80 redirect_uri=redirect_uri, state=state_string, scope=scope
81 )
82 return str(authorization_url)
83 except Exception as e:
84 logger.error(f"{provider} Authorization URL 생성 오류: {e}")
85 raise ValueError(f"{provider.capitalize()} Authorization URL 생성 실패")
87 @staticmethod
88 async def get_access_token_and_profile(
89 provider: str,
90 code: str,
91 redirect_uri: str,
92 ) -> tuple[BaseOAuthToken, Union[GoogleProfile, KakaoProfile, NaverProfile]]:
93 """
94 1) Access Token 획득
95 2) Provider별 Profile API 호출
96 3) (token_data, profile_data) 반환
97 """
98 client = OAuthManager.get_provider_client(provider)
99 try:
100 token_response = await client.get_access_token(
101 code=code,
102 redirect_uri=redirect_uri,
103 )
104 except Exception as e:
105 logger.error(f"{provider} 토큰 획득 오류: {e}")
106 raise ValueError(f"{provider.capitalize()} 토큰 획득 실패")
107 token_data: BaseOAuthToken
108 profile_data: Union[GoogleProfile, KakaoProfile, NaverProfile]
109 async with httpx.AsyncClient() as httpc:
110 if provider == "google":
111 token_data = GoogleToken(**token_response)
112 res = await httpc.get(
113 "https://www.googleapis.com/oauth2/v2/userinfo",
114 params={"access_token": token_data.access_token},
115 )
116 res.raise_for_status()
117 raw_profile = res.json()
118 profile_data = GoogleProfile(**raw_profile)
119 elif provider == "kakao":
120 token_data = KakaoToken(**token_response)
121 res = await httpc.get(
122 "https://kapi.kakao.com/v2/user/me",
123 headers={"Authorization": f"Bearer {token_data.access_token}"},
124 )
125 res.raise_for_status()
126 raw_profile = res.json()
127 profile_data = KakaoProfile(**raw_profile)
128 elif provider == "naver":
129 token_data = NaverToken(**token_response)
130 res = await httpc.get(
131 "https://openapi.naver.com/v1/nid/me",
132 headers={"Authorization": f"Bearer {token_data.access_token}"},
133 )
134 res.raise_for_status()
135 raw_profile = res.json()
136 profile_data = NaverProfile(**raw_profile["response"])
137 else:
138 raise ValueError(f"Not implemented provider: {provider}")
139 return token_data, profile_data
142oauth_manager = OAuthManager()