PySimpleSocial/src/endpoints/auth.py

130 lines
3.6 KiB
Python
Raw Normal View History

2023-03-13 15:59:03 +01:00
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")