| 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, |
| ) |
|
|