유튜브 인증 로직 수정

김성경 2026-05-13 15:54:24 +09:00
parent ec3e0159e8
commit f47dd423c5
4 changed files with 26 additions and 10 deletions

View File

@ -62,6 +62,7 @@ def _build_redirect_url(is_success: bool, params: dict) -> str:
async def start_connect( async def start_connect(
platform: SocialPlatform, platform: SocialPlatform,
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_session),
) -> SocialConnectResponse: ) -> SocialConnectResponse:
""" """
소셜 계정 연동 시작 소셜 계정 연동 시작
@ -77,6 +78,7 @@ async def start_connect(
return await social_account_service.start_connect( return await social_account_service.start_connect(
user_uuid=current_user.user_uuid, user_uuid=current_user.user_uuid,
platform=platform, platform=platform,
session=session,
) )

View File

@ -24,12 +24,13 @@ class BaseOAuthClient(ABC):
platform: SocialPlatform platform: SocialPlatform
@abstractmethod @abstractmethod
def get_authorization_url(self, state: str) -> str: def get_authorization_url(self, state: str, force_consent: bool = False) -> str:
""" """
OAuth 인증 URL 생성 OAuth 인증 URL 생성
Args: Args:
state: CSRF 방지용 state 토큰 state: CSRF 방지용 state 토큰
force_consent: True면 동의 화면 강제 표시 (refresh_token 재발급 필요 )
Returns: Returns:
str: OAuth 인증 페이지 URL str: OAuth 인증 페이지 URL

View File

@ -43,12 +43,13 @@ class YouTubeOAuthClient(BaseOAuthClient):
self.client_secret = social_oauth_settings.YOUTUBE_CLIENT_SECRET self.client_secret = social_oauth_settings.YOUTUBE_CLIENT_SECRET
self.redirect_uri = social_oauth_settings.YOUTUBE_REDIRECT_URI self.redirect_uri = social_oauth_settings.YOUTUBE_REDIRECT_URI
def get_authorization_url(self, state: str) -> str: def get_authorization_url(self, state: str, force_consent: bool = False) -> str:
""" """
Google OAuth 인증 URL 생성 Google OAuth 인증 URL 생성
Args: Args:
state: CSRF 방지용 state 토큰 state: CSRF 방지용 state 토큰
force_consent: True면 동의 화면 강제 표시하여 refresh_token 재발급
Returns: Returns:
str: Google OAuth 인증 페이지 URL str: Google OAuth 인증 페이지 URL
@ -58,12 +59,12 @@ class YouTubeOAuthClient(BaseOAuthClient):
"redirect_uri": self.redirect_uri, "redirect_uri": self.redirect_uri,
"response_type": "code", "response_type": "code",
"scope": " ".join(YOUTUBE_SCOPES), "scope": " ".join(YOUTUBE_SCOPES),
"access_type": "offline", # refresh_token 받기 위해 필요 "access_type": "offline",
"prompt": "select_account", # 계정 선택만 표시 (동의 화면은 최초 1회만) "prompt": "consent" if force_consent else "select_account",
"state": state, "state": state,
} }
url = f"{self.AUTHORIZATION_URL}?{urlencode(params)}" url = f"{self.AUTHORIZATION_URL}?{urlencode(params)}"
logger.debug(f"[YOUTUBE_OAUTH] 인증 URL 생성: {url[:100]}...") logger.debug(f"[YOUTUBE_OAUTH] 인증 URL 생성 - force_consent: {force_consent}, url: {url[:100]}...")
return url return url
async def exchange_code(self, code: str) -> OAuthTokenResponse: async def exchange_code(self, code: str) -> OAuthTokenResponse:

View File

@ -59,15 +59,18 @@ class SocialAccountService:
self, self,
user_uuid: str, user_uuid: str,
platform: SocialPlatform, platform: SocialPlatform,
session: AsyncSession,
) -> SocialConnectResponse: ) -> SocialConnectResponse:
""" """
소셜 계정 연동 시작 소셜 계정 연동 시작
OAuth 인증 URL을 생성하고 state 토큰을 저장합니다. OAuth 인증 URL을 생성하고 state 토큰을 저장합니다.
기존 연동 계정에 refresh_token이 없으면 동의 화면을 강제 표시합니다.
Args: Args:
user_uuid: 사용자 UUID user_uuid: 사용자 UUID
platform: 연동할 플랫폼 platform: 연동할 플랫폼
session: DB 세션
Returns: Returns:
SocialConnectResponse: OAuth 인증 URL state 토큰 SocialConnectResponse: OAuth 인증 URL state 토큰
@ -77,10 +80,19 @@ class SocialAccountService:
f"user_uuid: {user_uuid}, platform: {platform.value}" f"user_uuid: {user_uuid}, platform: {platform.value}"
) )
# 1. state 토큰 생성 (CSRF 방지) # 1. 기존 계정의 refresh_token 존재 여부 확인
existing_account = await self.get_account_by_platform(user_uuid, platform, session)
force_consent = not (existing_account and existing_account.refresh_token)
logger.debug(
f"[SOCIAL] OAuth prompt 결정 - force_consent: {force_consent}, "
f"has_account: {existing_account is not None}, "
f"has_refresh_token: {bool(existing_account and existing_account.refresh_token)}"
)
# 2. state 토큰 생성 (CSRF 방지)
state = secrets.token_urlsafe(32) state = secrets.token_urlsafe(32)
# 2. state를 Redis에 저장 (user_uuid 포함) # 3. state를 Redis에 저장 (user_uuid 포함)
state_key = f"{self.STATE_KEY_PREFIX}{state}" state_key = f"{self.STATE_KEY_PREFIX}{state}"
state_data = { state_data = {
"user_uuid": user_uuid, "user_uuid": user_uuid,
@ -89,13 +101,13 @@ class SocialAccountService:
await redis_client.setex( await redis_client.setex(
state_key, state_key,
social_oauth_settings.OAUTH_STATE_TTL_SECONDS, social_oauth_settings.OAUTH_STATE_TTL_SECONDS,
json.dumps(state_data), # JSON으로 직렬화 json.dumps(state_data),
) )
logger.debug(f"[SOCIAL] OAuth state 저장 - key: {state_key}") logger.debug(f"[SOCIAL] OAuth state 저장 - key: {state_key}")
# 3. OAuth 클라이언트에서 인증 URL 생성 # 4. OAuth 클라이언트에서 인증 URL 생성
oauth_client = get_oauth_client(platform) oauth_client = get_oauth_client(platform)
auth_url = oauth_client.get_authorization_url(state) auth_url = oauth_client.get_authorization_url(state, force_consent=force_consent)
logger.info(f"[SOCIAL] OAuth URL 생성 완료 - platform: {platform.value}") logger.info(f"[SOCIAL] OAuth URL 생성 완료 - platform: {platform.value}")