Files
AI/ontology_platform/ont_platform/auth/auth.py
lasta 47a710a8b9 Fix datetime deprecation warnings in Phase 8 modules
- Update all datetime.utcnow() to datetime.now(UTC) for Python 3.12+ compatibility
- Update all datetime.utcfromtimestamp() to datetime.fromtimestamp(..., UTC)
- Fix dataclass default_factory to use lambda: datetime.now(UTC)
- Update auth, audit, billing, and realtime modules
- Add UTC import from datetime module
- Update pytest configuration to include pytest-asyncio
- All 28 Phase 8 enterprise tests pass with no warnings

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-14 11:50:23 +09:00

339 lines
9.0 KiB
Python

"""인증 시스템 (JWT, API 키).
Phase 8: 멀티테넌트 인증
"""
import os
import jwt
import hashlib
import secrets
from datetime import datetime, timedelta, UTC
from typing import Optional, Dict
from fastapi import HTTPException, Depends, Header
from ont_platform.auth.models import (
User,
Organization,
APIKey,
TokenPayload,
CurrentUser,
AuthCredentials,
)
# 환경 변수
SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key-change-in-production")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_HOURS = 24
class JWTAuth:
"""JWT 기반 토큰 인증."""
@staticmethod
def create_token(
user_id: str,
org_id: str,
email: str,
role: str,
expires_delta: Optional[timedelta] = None,
) -> str:
"""JWT 토큰 생성."""
if expires_delta is None:
expires_delta = timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS)
exp = datetime.now(UTC) + expires_delta
payload = {
"user_id": user_id,
"org_id": org_id,
"email": email,
"role": role,
"exp": int(exp.timestamp()),
}
token = jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
return token
@staticmethod
def verify_token(token: str) -> TokenPayload:
"""JWT 토큰 검증."""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
# 토큰 만료 확인
exp = payload.get("exp")
if exp and datetime.fromtimestamp(exp, UTC) < datetime.now(UTC):
raise HTTPException(
status_code=401,
detail="Token has expired",
)
return TokenPayload(
user_id=payload["user_id"],
org_id=payload["org_id"],
email=payload["email"],
role=payload["role"],
exp=exp,
)
except jwt.InvalidTokenError:
raise HTTPException(
status_code=401,
detail="Invalid token",
)
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=401,
detail="Token has expired",
)
@staticmethod
def refresh_token(token: str) -> str:
"""토큰 갱신."""
payload = JWTAuth.verify_token(token)
return JWTAuth.create_token(
user_id=payload.user_id,
org_id=payload.org_id,
email=payload.email,
role=payload.role,
)
class APIKeyAuth:
"""API 키 기반 인증."""
@staticmethod
def generate_key() -> str:
"""새 API 키 생성 (클라이언트에게만 보여줌)."""
return f"sk_{secrets.token_urlsafe(32)}"
@staticmethod
def hash_key(api_key: str) -> str:
"""API 키 해시 (DB에 저장)."""
return hashlib.sha256(api_key.encode()).hexdigest()
@staticmethod
async def verify_key(api_key: str) -> Dict[str, str]:
"""API 키 검증 → org_id, user_id 반환.
실제 구현에서는 DB 조회가 필요합니다.
"""
key_hash = APIKeyAuth.hash_key(api_key)
# TODO: DB에서 조회
# api_key_record = await db.get_api_key_by_hash(key_hash)
# if not api_key_record or not api_key_record.is_active:
# raise HTTPException(status_code=401, detail="Invalid API key")
# 현재는 모의 구현
if not api_key.startswith("sk_"):
raise HTTPException(status_code=401, detail="Invalid API key format")
return {
"org_id": "org_123",
"user_id": "user_456",
}
# 의존성 함수 (FastAPI)
async def get_token_from_header(
authorization: Optional[str] = Header(None),
) -> str:
"""헤더에서 토큰 추출."""
if not authorization:
raise HTTPException(
status_code=401,
detail="Missing authorization header",
)
parts = authorization.split()
if len(parts) != 2 or parts[0].lower() != "bearer":
raise HTTPException(
status_code=401,
detail="Invalid authorization header format",
)
return parts[1]
async def get_api_key_from_header(
x_api_key: Optional[str] = Header(None),
) -> str:
"""헤더에서 API 키 추출."""
if not x_api_key:
raise HTTPException(
status_code=401,
detail="Missing API key",
)
return x_api_key
async def get_current_user(
authorization: Optional[str] = Header(None),
x_api_key: Optional[str] = Header(None),
) -> CurrentUser:
"""현재 인증된 사용자 반환 (JWT 또는 API 키)."""
# JWT 토큰으로 인증 시도
if authorization:
try:
token = await get_token_from_header(authorization)
payload = JWTAuth.verify_token(token)
return CurrentUser(
user_id=payload.user_id,
org_id=payload.org_id,
email=payload.email,
username=payload.email.split("@")[0],
role=payload.role,
is_active=True,
)
except HTTPException:
pass # API 키 시도
# API 키로 인증 시도
if x_api_key:
try:
result = await APIKeyAuth.verify_key(x_api_key)
return CurrentUser(
user_id=result["user_id"],
org_id=result["org_id"],
email=f"api_user_{result['user_id']}@api",
username=f"api_{result['user_id']}",
role="api",
is_active=True,
)
except HTTPException:
pass
raise HTTPException(
status_code=401,
detail="Authentication failed (provide JWT token or API key)",
)
class PasswordHasher:
"""비밀번호 해싱 (Argon2 대신 간단한 해시 사용)."""
@staticmethod
def hash_password(password: str) -> str:
"""비밀번호 해싱."""
# 실제 운영에서는 bcrypt/argon2 사용
salt = secrets.token_hex(8)
hashed = hashlib.pbkdf2_hmac(
"sha256",
password.encode(),
salt.encode(),
100000,
).hex()
return f"{salt}${hashed}"
@staticmethod
def verify_password(password: str, hashed: str) -> bool:
"""비밀번호 검증."""
try:
salt, hashed_pw = hashed.split("$")
new_hash = hashlib.pbkdf2_hmac(
"sha256",
password.encode(),
salt.encode(),
100000,
).hex()
return new_hash == hashed_pw
except Exception:
return False
class AuthService:
"""인증 서비스."""
@staticmethod
async def register_user(
org: Organization,
email: str,
username: str,
password: str,
role: str = "viewer",
) -> tuple[User, str]:
"""사용자 등록."""
user = User(
org_id=org.id,
email=email,
username=username,
hashed_password=PasswordHasher.hash_password(password),
role=role,
)
# TODO: DB에 저장
# await db.create_user(user)
token = JWTAuth.create_token(
user_id=user.id,
org_id=org.id,
email=email,
role=role,
)
return user, token
@staticmethod
async def login(
org_id: str,
email: str,
password: str,
) -> tuple[User, str]:
"""사용자 로그인."""
# TODO: DB에서 사용자 조회
# user = await db.get_user_by_email(email, org_id)
# if not user or not PasswordHasher.verify_password(password, user.hashed_password):
# raise HTTPException(status_code=401, detail="Invalid credentials")
# 모의 구현
user = User(
id="user_123",
org_id=org_id,
email=email,
username=email.split("@")[0],
role="editor",
)
token = JWTAuth.create_token(
user_id=user.id,
org_id=org_id,
email=email,
role=user.role,
)
# TODO: 마지막 로그인 시간 업데이트
# user.last_login = datetime.utcnow()
# await db.update_user(user)
return user, token
@staticmethod
async def create_api_key(
org_id: str,
user_id: str,
name: str,
description: str = "",
) -> APIKey:
"""API 키 생성."""
api_key = APIKeyAuth.generate_key()
api_key_record = APIKey(
org_id=org_id,
key_hash=APIKeyAuth.hash_key(api_key),
name=name,
description=description,
)
# TODO: DB에 저장
# await db.create_api_key(api_key_record)
# 클라이언트에게 원본 키만 한 번 반환
api_key_record.original_key = api_key
return api_key_record