Commit 49a102a2 authored by Richard Glosner's avatar Richard Glosner
Browse files

Merge branch '316-remove-csrf-on-graphql-api-rethink-authentication-cookies' into 'main'

Resolve "Remove CSRF on GraphQL API, rethink authentication/cookies"

See merge request inject/backend!308
parents f3d46c69 2e6cd5bb
Loading
Loading
Loading
Loading

aai/middleware.py

0 → 100644
+32 −0
Original line number Diff line number Diff line
from importlib import import_module

from django.conf import settings
from django.contrib.sessions.backends.base import UpdateError
from django.contrib.sessions.exceptions import SessionInterrupted
from django.utils.deprecation import MiddlewareMixin


class SessionMiddleware(MiddlewareMixin):
    def __init__(self, get_response=None):
        super().__init__(get_response)
        engine = import_module(settings.SESSION_ENGINE)
        self.SessionStore = engine.SessionStore

    def process_request(self, request):
        setattr(request, "_dont_enforce_csrf_checks", True)
        sessionid = request.META.get("HTTP_SESSION_ID")
        request.session = self.SessionStore(sessionid)

    def process_response(self, request, response):
        modified = request.session.modified
        empty = request.session.is_empty()
        if modified and not empty and response.status_code != 500:
            try:
                request.session.save()
            except UpdateError:
                raise SessionInterrupted(
                    "The request's session was deleted before the "
                    "request completed. The user may have logged "
                    "out in a concurrent request, for example."
                )
        return response
+0 −44
Original line number Diff line number Diff line
import graphene
from django.conf import settings
from django.contrib.auth import authenticate, logout, login
from django.core.exceptions import ValidationError

from aai.decorators import logged_in
from common_lib.logger import logger, log_user_msg
from common_lib.schema.types import UserType
from user.email.email_sender import send_password_change_notification
from user.models import User


class LoginMutation(graphene.Mutation):
    class Arguments:
        username = graphene.String(required=True)
        password = graphene.String(required=True)

    user = graphene.Field(UserType, required=True)

    @classmethod
    def mutate(
        cls,
        root,
        info,
        username: str,
        password: str,
    ) -> graphene.Mutation:
        user: User = authenticate(
            username=username, password=password, request=info.context
        )
        login(info.context, user)
        logger.info(log_user_msg(info.context, user) + "successful login")
        # TODO: implement attempts limiter
        return LoginMutation(user=user)


class LogoutMutation(graphene.Mutation):
    logged_out = graphene.Boolean(required=True)

    @classmethod
    @logged_in
    def mutate(cls, root, info) -> graphene.Mutation:
        logger.info(
            log_user_msg(info.context, info.context.user) + "logged out"
        )
        logout(info.context)
        return LogoutMutation(logged_out=True)


class PasswordChange(graphene.Mutation):
@@ -97,10 +57,6 @@ class PasswordChange(graphene.Mutation):


class Mutation(graphene.ObjectType):
    login = LoginMutation.Field(description="Mutation for logging in an user")
    logout = LogoutMutation.Field(
        description="Mutation for logging out an user"
    )
    password_change = PasswordChange.Field(
        description="Mutation for changing a user's password"
    )

aai/urls.py

0 → 100644
+21 −0
Original line number Diff line number Diff line
from django.urls import path

from aai import views

urlpatterns = [
    path(
        "auth/login/",
        views.LoginView.as_view(),
        name="login",
    ),
    path(
        "auth/logout/",
        views.Logout.as_view(),
        name="logout",
    ),
    path(
        "auth/session/",
        views.CheckSession.as_view(),
        name="session",
    ),
]
+58 −2
Original line number Diff line number Diff line
from django.shortcuts import render
from django.contrib.auth import authenticate, login, logout

# Create your views here.
from rest_framework import parsers
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView

from common_lib.exceptions import ApiException
from common_lib.logger import logger, log_user_msg
from user.models import User


class LoginView(APIView):
    parser_classes = [parsers.JSONParser]

    def post(self, request: Request, *args, **kwargs):
        """Check credentials and log user in."""
        username = request.data.get("username")
        if not username:
            raise ApiException("Username is required")
        password = request.data.get("password")
        if not password:
            raise ApiException("Password is required")

        user: User = authenticate(
            username=username, password=password, request=request
        )
        login(request, user)
        logger.info(log_user_msg(request, request.user) + "successful login")
        return Response(
            {
                "sessionid": request.session.session_key,
            }
        )


class Logout(APIView):
    parser_classes = [parsers.JSONParser]

    def post(self, request: Request, *args, **kwargs):
        """Log out a user."""
        logger.info(log_user_msg(request, request.user) + "logged out")
        logout(request)
        return Response(
            {
                "sessionid": None,
            }
        )


class CheckSession(APIView):
    parser_classes = [parsers.JSONParser]

    def get(self, request: Request, *args, **kwargs):
        """Check if the session is still valid."""
        session = (
            None if request.session.is_empty() else request.session.session_key
        )
        return Response({"sessionid": session})
+0 −3
Original line number Diff line number Diff line
from django.conf import settings
from django.http import FileResponse
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import ensure_csrf_cookie
from rest_framework import parsers
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
@@ -36,7 +34,6 @@ class RetrieveExerciseLogsView(APIView):
class BackendVersionView(APIView):
    renderer_classes = [JSONRenderer]

    @method_decorator(ensure_csrf_cookie)
    def get(self, request, *args, **kwargs):
        """
        Get the backend version
Loading