""" JWT 토큰 유틸리티 Access Token과 Refresh Token의 생성, 검증, 해시 기능을 제공합니다. """ import hashlib import logging from datetime import datetime, timedelta from typing import Optional from jose import JWTError, jwt from jose.exceptions import ExpiredSignatureError, JWTClaimsError from app.utils.timezone import now from config import jwt_settings logger = logging.getLogger(__name__) def create_access_token(user_uuid: str) -> str: """ JWT 액세스 토큰 생성 Args: user_uuid: 사용자 UUID Returns: JWT 액세스 토큰 문자열 """ expire = now() + timedelta( minutes=jwt_settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES ) to_encode = { "sub": user_uuid, "exp": expire, "type": "access", } token = jwt.encode( to_encode, jwt_settings.JWT_SECRET, algorithm=jwt_settings.JWT_ALGORITHM, ) logger.debug( f"[JWT] Access Token 발급 - user_uuid: {user_uuid}, " f"expires: {expire}, token: ...{token[-20:]}" ) return token def create_refresh_token(user_uuid: str) -> str: """ JWT 리프레시 토큰 생성 Args: user_uuid: 사용자 UUID Returns: JWT 리프레시 토큰 문자열 """ expire = now() + timedelta( days=jwt_settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS ) to_encode = { "sub": user_uuid, "exp": expire, "type": "refresh", } token = jwt.encode( to_encode, jwt_settings.JWT_SECRET, algorithm=jwt_settings.JWT_ALGORITHM, ) logger.debug( f"[JWT] Refresh Token 발급 - user_uuid: {user_uuid}, " f"expires: {expire}, token: ...{token[-20:]}" ) return token def decode_token(token: str) -> Optional[dict]: """ JWT 토큰 디코딩 Args: token: JWT 토큰 문자열 Returns: 디코딩된 페이로드 딕셔너리, 실패 시 None """ try: payload = jwt.decode( token, jwt_settings.JWT_SECRET, algorithms=[jwt_settings.JWT_ALGORITHM], ) logger.debug( f"[JWT] 토큰 디코딩 성공 - type: {payload.get('type')}, " f"sub: {payload.get('sub')}, exp: {payload.get('exp')}, " f"token: ...{token[-20:]}" ) return payload except ExpiredSignatureError: logger.info(f"[JWT] 토큰 만료 - token: ...{token[-20:]}") return None except JWTClaimsError as e: logger.warning( f"[JWT] 클레임 검증 실패 - error: {e}, token: ...{token[-20:]}" ) return None except JWTError as e: logger.warning( f"[JWT] 토큰 디코딩 실패 - error: {type(e).__name__}: {e}, " f"token: ...{token[-20:]}" ) return None def is_token_expired(token: str) -> bool: """ 토큰이 만료됐는지 확인 (서명/형식은 유효하지만 exp 초과인 경우) Returns: True: 서명은 유효하나 만료된 토큰, False: 형식/서명 자체가 잘못된 토큰 """ try: payload = jwt.decode( token, jwt_settings.JWT_SECRET, algorithms=[jwt_settings.JWT_ALGORITHM], options={"verify_exp": False}, ) exp = payload.get("exp") if exp is None: return False return datetime.fromtimestamp(exp) < datetime.now() except JWTError: return False def get_token_hash(token: str) -> str: """ 토큰의 SHA-256 해시값 생성 리프레시 토큰을 DB에 저장할 때 원본 대신 해시값을 저장합니다. Args: token: 해시할 토큰 문자열 Returns: 토큰의 SHA-256 해시값 (64자 hex 문자열) """ return hashlib.sha256(token.encode()).hexdigest() def get_refresh_token_expires_at() -> datetime: """ 리프레시 토큰 만료 시간 계산 Returns: 리프레시 토큰 만료 datetime (로컬 시간) """ return now().replace(tzinfo=None) + timedelta( days=jwt_settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS ) def get_access_token_expire_seconds() -> int: """ 액세스 토큰 만료 시간(초) 반환 Returns: 액세스 토큰 만료 시간 (초) """ return jwt_settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES * 60