Files
thePlug/backend/app/core/security.py
2026-05-07 09:21:45 +02:00

116 lines
3.1 KiB
Python

from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from typing import Annotated
from uuid import uuid4
from authlib.jose import jwt # type: ignore
from authlib.jose.errors import JoseError # type: ignore
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from pwdlib import PasswordHash
from pwdlib.hashers.argon2 import Argon2Hasher
from app.core.config import settings
from app.models.auth import AccessTokenPayload, AuthUserDocument
from app.repositories.auth import get_user_by_username, is_token_revoked
pwd_context = PasswordHash((Argon2Hasher(),))
bearer_scheme = HTTPBearer(auto_error=False)
@dataclass(slots=True)
class AuthContext:
token: str
user: AuthUserDocument
payload: AccessTokenPayload
class InvalidTokenError(Exception):
pass
def token_auth_error() -> HTTPException:
return HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain_password: str, password_hash: str) -> bool:
return pwd_context.verify(plain_password, password_hash)
def create_access_token(*, subject: str) -> str:
now = datetime.now(UTC)
expires_delta = timedelta(minutes=settings.JWT_EXPIRE_MINUTES)
expires_at = int((now + expires_delta).timestamp())
header = {
"alg": settings.JWT_ALGORITHM,
"typ": "JWT",
}
payload = {
"sub": subject,
"jti": str(uuid4()),
"exp": expires_at,
}
token = jwt.encode(header, payload, settings.JWT_SECRET_KEY)
if isinstance(token, bytes):
return token.decode("utf-8")
return token
def decode_access_token(token: str) -> AccessTokenPayload:
try:
claims = jwt.decode(token, settings.JWT_SECRET_KEY)
claims.validate()
return AccessTokenPayload.model_validate(dict(claims))
except JoseError, ValueError, TypeError:
raise InvalidTokenError from None
async def authenticate_token(token: str) -> AuthContext:
try:
payload = decode_access_token(token)
except InvalidTokenError:
raise InvalidTokenError from None
if await is_token_revoked(payload.jti):
raise InvalidTokenError
user = await get_user_by_username(payload.sub)
if user is None:
raise InvalidTokenError
return AuthContext(token=token, user=user, payload=payload)
async def get_auth_context(
credentials: Annotated[
HTTPAuthorizationCredentials | None,
Depends(bearer_scheme),
],
) -> AuthContext:
if credentials is None or credentials.scheme.lower() != "bearer":
raise token_auth_error()
try:
return await authenticate_token(credentials.credentials)
except InvalidTokenError:
raise token_auth_error() from None
async def require_authenticated_user(
auth_context: Annotated[AuthContext, Depends(get_auth_context)],
) -> AuthUserDocument:
return auth_context.user