PySimpleSocial/src/endpoints/auth.py

130 lines
3.6 KiB
Python

import bcrypt
from datetime import timedelta
from fastapi.exceptions import HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from fastapi import APIRouter as FastAPI, Depends, Response, Request
from config import (
LOGGER,
SESSION_EXPIRE_LIMIT,
COOKIE_SAMESITE_POLICY,
COOKIE_DOMAIN,
MANAGER,
SESSION_COOKIE_NAME,
LIMITER,
SECURE_COOKIE,
COOKIE_PATH,
COOKIE_HTTPONLY,
UNVERIFIED_MANAGER,
)
from responses import (
Response as APIResponse,
UnprocessableEntity,
BadRequest,
)
from orm.users import (
UserModel,
PrivateUserModel,
get_user_by_username,
)
router = FastAPI()
@router.post(
"/user",
tags=["Users"],
status_code=200,
responses={
200: {"model": APIResponse},
400: {"model": BadRequest},
422: {"model": UnprocessableEntity},
},
)
@LIMITER.limit("5/minute")
async def login(
request: Request, response: Response, data: OAuth2PasswordRequestForm = Depends()
):
"""
Performs user authentication. Endpoint is limited to 5 hits per minute
"""
if request.cookies.get(SESSION_COOKIE_NAME):
raise HTTPException(status_code=400, detail="Please logout first")
username = data.username
if len(username) > 32:
raise HTTPException(
status_code=413, detail="Authentication failed: username is too long"
)
try:
password = data.password.encode()
if len(password) > 72:
raise HTTPException(
status_code=413, detail="Authentication failed: password is too long"
)
except UnicodeEncodeError as e:
LOGGER.warning(
f"An error occurred while attempting to decode password for user {username} -> {type(e).__name__}: {e}"
)
raise HTTPException(
status_code=413,
detail="Authentication failed: invalid characters in password",
)
if not (
user := await get_user_by_username(
username, include_secrets=True, restricted_ok=True
)
):
raise HTTPException(
status_code=413,
detail="Authentication failed: the user does not exist",
)
user: PrivateUserModel
if not bcrypt.checkpw(password, user.password_hash):
raise HTTPException(
status_code=413,
detail="Authentication failed: password mismatch",
)
token = MANAGER.create_access_token(
expires=timedelta(seconds=SESSION_EXPIRE_LIMIT),
data={"sub": str(user.public_id)},
)
response.set_cookie(
secure=SECURE_COOKIE,
key=SESSION_COOKIE_NAME,
max_age=SESSION_EXPIRE_LIMIT,
value=token,
httponly=COOKIE_HTTPONLY,
samesite=COOKIE_SAMESITE_POLICY,
domain=COOKIE_DOMAIN or None,
path=COOKIE_PATH or "/",
)
return APIResponse(status_code=200, msg="Authentication successful")
@router.get(
"/user/logout",
tags=["Users"],
status_code=200,
responses={200: {"model": APIResponse}, 422: {"model": UnprocessableEntity}},
)
@LIMITER.limit("5/minute")
async def logout(
request: Request, response: Response, _user: UserModel = Depends(UNVERIFIED_MANAGER)
):
"""
Deletes a user's session cookie, logging them
out. Endpoint is limited to 5 hits per minute
"""
response.delete_cookie(
secure=SECURE_COOKIE,
key=SESSION_COOKIE_NAME,
httponly=COOKIE_HTTPONLY,
samesite=COOKIE_SAMESITE_POLICY,
domain=COOKIE_DOMAIN or None,
path=COOKIE_PATH or "/",
)
return APIResponse(status_code=200, msg="Logged out")