diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0621541..d466903 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -53,3 +53,5 @@ jobs: tox -v env: DJANGO: ${{ matrix.django-version }} + DESCOPE_PROJECT_ID: P2ZRsmAQw8MKG78knGZ9GXWRqxM5 + DESCOPE_MANAGEMENT_KEY: ${{ secrets.DESCOPE_MANAGEMENT_KEY }} diff --git a/django_descope/authentication.py b/django_descope/authentication.py index 11831bf..f88a503 100644 --- a/django_descope/authentication.py +++ b/django_descope/authentication.py @@ -1,4 +1,5 @@ import logging +from typing import Any, Union from descope import REFRESH_SESSION_COOKIE_NAME, SESSION_COOKIE_NAME, SESSION_TOKEN_NAME from descope.exceptions import AuthException @@ -13,13 +14,21 @@ logger = logging.getLogger(__name__) +def add_tokens_to_request(session: Any, session_token: str, refresh_token: str): + session[SESSION_COOKIE_NAME] = session_token + session[REFRESH_SESSION_COOKIE_NAME] = refresh_token + session.save() + + class DescopeAuthentication(BaseBackend): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def authenticate(self, request: HttpRequest): - session_token = request.session.get(SESSION_COOKIE_NAME) - refresh_token = request.session.get(REFRESH_SESSION_COOKIE_NAME) + def authenticate(self, request: Union[HttpRequest, None], **kwargs): + if request is None: + return None + session_token = request.session.get(SESSION_COOKIE_NAME, "") + refresh_token = request.session.get(REFRESH_SESSION_COOKIE_NAME, "") logger.debug("Validating (and refreshing) Descope session") try: @@ -42,13 +51,11 @@ def authenticate(self, request: HttpRequest): if settings.DEBUG: # Contains sensitive information, so only log in DEBUG mode logger.debug(validated_session) - return self.get_user(request, validated_session, refresh_token) - - def get_user(self, request: HttpRequest, validated_session, refresh_token): if validated_session: username = validated_session[SESSION_TOKEN_NAME]["sub"] - user, created = DescopeUser.objects.get_or_create(username=username) + user, _ = DescopeUser.objects.get_or_create(username=username) user.sync(validated_session, refresh_token) request.session[SESSION_COOKIE_NAME] = user.session_token["jwt"] return user - return None + else: + return None diff --git a/django_descope/middleware.py b/django_descope/middleware.py index bcffcff..367bffc 100644 --- a/django_descope/middleware.py +++ b/django_descope/middleware.py @@ -1,17 +1,18 @@ import logging from django.contrib.auth import login -from django.http import HttpRequest, HttpResponse +from django.http import HttpRequest +from django.utils.deprecation import MiddlewareMixin from .authentication import DescopeAuthentication logger = logging.getLogger(__name__) -class DescopeMiddleware: +class DescopeMiddleware(MiddlewareMixin): _auth = DescopeAuthentication() - def __init__(self, get_response: HttpResponse = None): + def __init__(self, get_response): self.get_response = get_response def __call__(self, request: HttpRequest): diff --git a/django_descope/views.py b/django_descope/views.py index 748f760..02cefef 100644 --- a/django_descope/views.py +++ b/django_descope/views.py @@ -6,7 +6,8 @@ from django.views import View from django.views.decorators.cache import never_cache -# User = get_user_model() +from .authentication import add_tokens_to_request + logger = logging.getLogger(__name__) @@ -19,8 +20,7 @@ def post(self, request: HttpRequest): refresh = request.POST.get(REFRESH_SESSION_COOKIE_NAME) if session and refresh: - request.session[SESSION_COOKIE_NAME] = session - request.session[REFRESH_SESSION_COOKIE_NAME] = refresh + add_tokens_to_request(request, session, refresh) return JsonResponse({"success": True}) return HttpResponseBadRequest() diff --git a/example_app/__init__.py b/example_app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example_app/test_admin.py b/example_app/test_admin.py new file mode 100644 index 0000000..fec80b7 --- /dev/null +++ b/example_app/test_admin.py @@ -0,0 +1,61 @@ +import json +import logging +import random +import string + +import descope +from descope import REFRESH_SESSION_TOKEN_NAME, SESSION_TOKEN_NAME +from django.test import TestCase + +from django_descope import descope_client +from django_descope.authentication import add_tokens_to_request + +logger = logging.getLogger(__name__) + + +def random_string(N: int) -> str: + return "".join(random.choices(string.ascii_lowercase + string.digits, k=N)) + + +class AdminLoginTestCase(TestCase): + delivery_method = descope.DeliveryMethod.EMAIL + login_id = f"test+{random_string(8)}@test.internal" + token: dict + + def setUp(self) -> None: + descope_client.mgmt.user.create_test_user( + self.login_id, role_names=["is_staff", "is_superuser"], verified_email=True + ) + resp = descope_client.mgmt.user.generate_otp_for_test_user( + self.delivery_method, + self.login_id, + ) + self.token = descope_client.otp.verify_code( + self.delivery_method, self.login_id, resp.get("code") + ) + + session = self.client.session + add_tokens_to_request( + session, + self.token[SESSION_TOKEN_NAME]["jwt"], + self.token[REFRESH_SESSION_TOKEN_NAME]["jwt"], + ) + + def test_test_user_can_login_to_admin(self): + """Test that if user has the right roles they can login to admin""" + + res = self.client.get("/debug") + self.assertEqual(res.status_code, 200) + + debug = json.loads(res.content) + self.assertEqual(debug["user"], self.token["userId"]) + self.assertEqual(debug["email"], self.login_id) + self.assertTrue(debug["is_authenticated"]) + self.assertTrue(debug["is_staff"]) + self.assertTrue(debug["is_superuser"]) + + res = self.client.get("/admin/") + self.assertEqual(res.status_code, 200, res.headers) + + def tearDown(self) -> None: + descope_client.mgmt.user.delete(self.login_id) diff --git a/example_app/views.py b/example_app/views.py index 127c0eb..30f4e3b 100644 --- a/example_app/views.py +++ b/example_app/views.py @@ -7,10 +7,6 @@ from django_descope import descope_client from django_descope.models import DescopeUser -from django.shortcuts import render - -from django.urls import path - logger = logging.getLogger(__name__) diff --git a/settings.py b/settings.py index 355b18f..bad7150 100644 --- a/settings.py +++ b/settings.py @@ -12,6 +12,7 @@ import os from pathlib import Path +from typing import List from dotenv import load_dotenv @@ -103,8 +104,7 @@ # Password validation # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators -AUTH_PASSWORD_VALIDATORS = [] # With descope, there's no need for passwords! - +AUTH_PASSWORD_VALIDATORS: List[str] = [] # With descope, there's no need for passwords! # Internationalization # https://docs.djangoproject.com/en/4.1/topics/i18n/ diff --git a/tox.ini b/tox.ini index 096d290..91f4e09 100644 --- a/tox.ini +++ b/tox.ini @@ -21,9 +21,11 @@ commands = python manage.py test extras= test +passenv= + DESCOPE_PROJECT_ID + DESCOPE_MANAGEMENT_KEY setenv= PYTHONDONTWRITEBYTECODE=1 - DESCOPE_PROJECT_ID=test deps= dj32: Django>=3.2,<3.3 dj40: Django>=4.0,<4.1