duyduc
Fix HF retrieval source dependency closure
14cd803
Raw
History Blame Contribute Delete
4.31 kB
from datetime import UTC, datetime, timedelta
from secrets import token_urlsafe
from typing import Any
import bcrypt
import httpx
import jwt
from google.auth.transport import requests as google_requests
from google.oauth2 import id_token as google_id_token
from src.config import Settings
from src.models.auth import TokenType
class SecurityError(ValueError):
pass
def require_jwt_secret(settings: Settings) -> str:
if settings.jwt_secret_key:
return settings.jwt_secret_key
if settings.app_env == "test":
return "test-jwt-secret-do-not-use"
raise SecurityError("JWT_SECRET_KEY is required for authentication.")
def hash_password(password: str) -> str:
password_bytes = password.encode("utf-8")
if len(password_bytes) > 72:
raise SecurityError("Password must be 72 bytes or fewer.")
return bcrypt.hashpw(password_bytes, bcrypt.gensalt()).decode("utf-8")
def verify_password(password: str, password_hash: str | None) -> bool:
if not password_hash:
return False
try:
return bcrypt.checkpw(password.encode("utf-8"), password_hash.encode("utf-8"))
except ValueError:
return False
def hash_token(token: str) -> str:
import hashlib
return hashlib.sha256(token.encode("utf-8")).hexdigest()
def create_access_token(
*,
user_id: str,
email: str,
settings: Settings,
now: datetime | None = None,
) -> tuple[str, datetime]:
issued_at = now or datetime.now(UTC)
expires_at = issued_at + timedelta(minutes=settings.access_token_expire_minutes)
payload = {
"sub": user_id,
"email": email,
"type": TokenType.ACCESS.value,
"exp": expires_at,
"iat": issued_at,
}
token = jwt.encode(payload, require_jwt_secret(settings), algorithm=settings.jwt_algorithm)
return token, expires_at
def create_refresh_token_value() -> str:
return token_urlsafe(48)
def create_oauth_state(settings: Settings, now: datetime | None = None) -> str:
issued_at = now or datetime.now(UTC)
expires_at = issued_at + timedelta(minutes=10)
payload = {
"nonce": token_urlsafe(24),
"type": TokenType.OAUTH_STATE.value,
"exp": expires_at,
"iat": issued_at,
}
return jwt.encode(payload, require_jwt_secret(settings), algorithm=settings.jwt_algorithm)
def decode_token(token: str, settings: Settings) -> dict[str, Any]:
try:
payload = jwt.decode(
token,
require_jwt_secret(settings),
algorithms=[settings.jwt_algorithm],
)
except jwt.PyJWTError as exc:
raise SecurityError("Invalid token.") from exc
if not isinstance(payload, dict):
raise SecurityError("Invalid token.")
return payload
def verify_oauth_state(state: str, settings: Settings) -> None:
payload = decode_token(state, settings)
if payload.get("type") != TokenType.OAUTH_STATE.value:
raise SecurityError("Invalid OAuth state.")
def build_google_authorization_url(settings: Settings, state: str) -> str:
params = {
"client_id": settings.google_client_id,
"redirect_uri": settings.google_redirect_uri,
"response_type": "code",
"scope": "openid email profile",
"state": state,
"access_type": "offline",
"prompt": "select_account",
}
return str(httpx.URL("https://accounts.google.com/o/oauth2/v2/auth", params=params))
async def exchange_google_code(code: str, settings: Settings) -> dict[str, Any]:
async with httpx.AsyncClient(timeout=10) as client:
response = await client.post(
"https://oauth2.googleapis.com/token",
data={
"code": code,
"client_id": settings.google_client_id,
"client_secret": settings.google_client_secret,
"redirect_uri": settings.google_redirect_uri,
"grant_type": "authorization_code",
},
)
response.raise_for_status()
return response.json()
def verify_google_id_token(id_token: str, settings: Settings) -> dict[str, Any]:
return google_id_token.verify_oauth2_token(
id_token,
google_requests.Request(),
settings.google_client_id,
clock_skew_in_seconds=30,
)