From 20e10ca8fc549400ed04af3fce0283de18a5f763 Mon Sep 17 00:00:00 2001 From: Mathieu Strypsteen Date: Wed, 13 Mar 2024 22:28:19 +0100 Subject: [PATCH] Implement authentication --- backend/app.py | 10 ++++- backend/controllers/auth/token_controller.py | 12 +++--- .../routes/dependencies/role_dependencies.py | 42 ++++++++++++------- backend/routes/errors/authentication.py | 4 ++ backend/routes/user.py | 25 ++++------- 5 files changed, 54 insertions(+), 39 deletions(-) diff --git a/backend/app.py b/backend/app.py index eb8b0b76..42693c23 100644 --- a/backend/app.py +++ b/backend/app.py @@ -6,7 +6,7 @@ from starlette.responses import JSONResponse from db.errors.database_errors import ActionAlreadyPerformedError, ItemNotFoundError, NoSuchRelationError -from routes.errors.authentication import InvalidRoleCredentialsError, NoAccessToSubjectError +from routes.errors.authentication import InvalidRoleCredentialsError, InvalidTokenError, NoAccessToSubjectError from routes.group import group_router from routes.login import login_router from routes.project import project_router @@ -87,5 +87,13 @@ def no_such_relation_error_handler(request: Request, exc: NoSuchRelationError) - ) +@app.exception_handler(InvalidTokenError) +def invalid_token_error_handler(request: Request, exc: NoSuchRelationError) -> JSONResponse: + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content={"detail": str(exc)}, + ) + + if __name__ == "__main__": uvicorn.run("app:app") diff --git a/backend/controllers/auth/token_controller.py b/backend/controllers/auth/token_controller.py index c574aa83..ceefcb37 100644 --- a/backend/controllers/auth/token_controller.py +++ b/backend/controllers/auth/token_controller.py @@ -12,14 +12,14 @@ def verify_token(token: str) -> int | None: with contextlib.suppress(jwt.ExpiredSignatureError, jwt.DecodeError): - payload = jwt.decode(token, jwt_secret) - return payload.get("userid", None) + payload = jwt.decode(token, jwt_secret, algorithms=["HS256"]) + return payload.get("uid", None) def create_token(user: UserDataclass) -> str: - exprire = datetime.now(UTC) + timedelta(days=1) + expire = datetime.now(UTC) + timedelta(days=1) to_encode: dict = { - "userid": user.id, - "exp": exprire, + "uid": user.id, + "exp": expire, } - return jwt.encode(to_encode, jwt_secret) + return jwt.encode(to_encode, jwt_secret, algorithm="HS256") diff --git a/backend/routes/dependencies/role_dependencies.py b/backend/routes/dependencies/role_dependencies.py index 9de95992..e16bfd1a 100644 --- a/backend/routes/dependencies/role_dependencies.py +++ b/backend/routes/dependencies/role_dependencies.py @@ -1,6 +1,8 @@ from fastapi import Depends +from fastapi.security import APIKeyHeader from sqlalchemy.orm import Session +from controllers.auth.token_controller import verify_token from db.sessions import get_session from domain.logic.admin import get_admin, is_user_admin from domain.logic.group import get_group @@ -15,33 +17,45 @@ InvalidAdminCredentialsError, InvalidStudentCredentialsError, InvalidTeacherCredentialsError, + InvalidTokenError, NoAccessToSubjectError, ) +auth_scheme = APIKeyHeader(name="cas") -def get_authenticated_user() -> int: - return 1 # Checken of een user bestaat en/of hij de juiste credentials heeft. +def get_authenticated_user(token: str = Depends(auth_scheme)) -> int: + uid = verify_token(token) + if uid is None: + raise InvalidTokenError + return uid -def get_authenticated_admin(session: Session = Depends(get_session)) -> AdminDataclass: - user_id = get_authenticated_user() - if not is_user_admin(session, user_id): + +def get_authenticated_admin( + session: Session = Depends(get_session), + uid: int = Depends(get_authenticated_user), +) -> AdminDataclass: + if not is_user_admin(session, uid): raise InvalidAdminCredentialsError - return get_admin(session, user_id) + return get_admin(session, uid) -def get_authenticated_teacher(session: Session = Depends(get_session)) -> TeacherDataclass: - user_id = get_authenticated_user() - if not is_user_teacher(session, user_id): +def get_authenticated_teacher( + session: Session = Depends(get_session), + uid: int = Depends(get_authenticated_user), +) -> TeacherDataclass: + if not is_user_teacher(session, uid): raise InvalidTeacherCredentialsError - return get_teacher(session, user_id) + return get_teacher(session, uid) -def get_authenticated_student(session: Session = Depends(get_session)) -> StudentDataclass: - user_id = get_authenticated_user() - if not is_user_student(session, user_id): +def get_authenticated_student( + session: Session = Depends(get_session), + uid: int = Depends(get_authenticated_user), +) -> StudentDataclass: + if not is_user_student(session, uid): raise InvalidStudentCredentialsError - return get_student(session, user_id) + return get_student(session, uid) def ensure_user_authorized_for_subject( diff --git a/backend/routes/errors/authentication.py b/backend/routes/errors/authentication.py index 4c51dd14..cf8acdec 100644 --- a/backend/routes/errors/authentication.py +++ b/backend/routes/errors/authentication.py @@ -16,3 +16,7 @@ class InvalidStudentCredentialsError(InvalidRoleCredentialsError): class NoAccessToSubjectError(Exception): ERROR_MESSAGE = "User doesn't have access to subject" + + +class InvalidTokenError(Exception): + ERROR_MESSAGE = "User is not authenticated" diff --git a/backend/routes/user.py b/backend/routes/user.py index 8202a0a9..e3544546 100644 --- a/backend/routes/user.py +++ b/backend/routes/user.py @@ -1,35 +1,24 @@ -from fastapi import APIRouter, Depends, Request, Response, status +from fastapi import APIRouter, Depends, Response, status from sqlalchemy.orm import Session -from controllers.auth.token_controller import verify_token from db.models.models import User from db.sessions import get_session from domain.logic.basic_operations import get, get_all from domain.logic.role_enum import Role -from domain.logic.user import convert_user, modify_user_roles +from domain.logic.user import convert_user, get_user, modify_user_roles from domain.models.APIUser import APIUser from domain.models.UserDataclass import UserDataclass -from routes.dependencies.role_dependencies import get_authenticated_admin +from routes.dependencies.role_dependencies import get_authenticated_admin, get_authenticated_user users_router = APIRouter() @users_router.get("/user") def get_current_user( - response: Response, - request: Request, session: Session = Depends(get_session), -) -> APIUser | None: - token: str | None = request.cookies.get("token") - if token is None: - response.status_code = 401 - return None - user_id: int | None = verify_token(token) - if user_id is None: - response.status_code = 401 - return None - user: UserDataclass = get(session, User, user_id).to_domain_model() - return convert_user(session, user) + uid: int = Depends(get_authenticated_user), +) -> APIUser: + return convert_user(session, get_user(session, uid)) @users_router.get("/users", dependencies=[Depends(get_authenticated_admin)]) @@ -39,7 +28,7 @@ def get_users(session: Session = Depends(get_session)) -> list[APIUser]: @users_router.get("/users/{uid}", dependencies=[Depends(get_authenticated_admin)]) -def get_user(uid: int, session: Session = Depends(get_session)) -> APIUser: +def admin_get_user(uid: int, session: Session = Depends(get_session)) -> APIUser: user: UserDataclass = get(session, User, uid).to_domain_model() return convert_user(session, user)