diff --git a/README.md b/README.md index 190e186..36e264f 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,16 @@ pip install django-descope The following settings are available to configure in your project `settings.py` +#### Required + +``` +DESCOPE_PROJECT_ID +``` + +#### Optional + ``` -DESCOPE_PROJECT_ID **Required** +DESCOPE_MANAGEMENT_KEY DESCOPE_IS_STAFF_ROLE DESCOPE_IS_SUPERUSER_ROLE ``` diff --git a/django_descope/__init__.py b/django_descope/__init__.py index e69de29..4744d1d 100644 --- a/django_descope/__init__.py +++ b/django_descope/__init__.py @@ -0,0 +1,7 @@ +from descope import DescopeClient + +from .settings import MANAGEMENT_KEY, PROJECT_ID + +descope_client = DescopeClient(project_id=PROJECT_ID, management_key=MANAGEMENT_KEY) + +all = [descope_client] diff --git a/django_descope/authentication.py b/django_descope/authentication.py index 88ac04e..11831bf 100644 --- a/django_descope/authentication.py +++ b/django_descope/authentication.py @@ -1,34 +1,32 @@ import logging -from descope import REFRESH_SESSION_COOKIE_NAME, SESSION_COOKIE_NAME, DescopeClient +from descope import REFRESH_SESSION_COOKIE_NAME, SESSION_COOKIE_NAME, SESSION_TOKEN_NAME from descope.exceptions import AuthException +from django.conf import settings from django.contrib.auth import logout from django.contrib.auth.backends import BaseBackend from django.http import HttpRequest +from . import descope_client from .models import DescopeUser -from .settings import PROJECT_ID logger = logging.getLogger(__name__) class DescopeAuthentication(BaseBackend): - _dclient = DescopeClient(PROJECT_ID) - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def authenticate(self, request: HttpRequest): - session = request.session.get(SESSION_COOKIE_NAME) - refresh = request.session.get(REFRESH_SESSION_COOKIE_NAME) + session_token = request.session.get(SESSION_COOKIE_NAME) + refresh_token = request.session.get(REFRESH_SESSION_COOKIE_NAME) logger.debug("Validating (and refreshing) Descope session") - logger.debug("session %s", session) - logger.debug("refresh %s", refresh) try: - validated_token = self._dclient.validate_and_refresh_session( - session, refresh + validated_session = descope_client.validate_and_refresh_session( + session_token, refresh_token ) + except AuthException as e: """ Ask forgiveness, not permission. @@ -41,14 +39,16 @@ def authenticate(self, request: HttpRequest): logout(request) return None - logger.debug(validated_token) - return self.get_user(request, validated_token, refresh) + 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_token=None, refresh_token=None): - if validated_token: - username = validated_token.get("userId") or validated_token.get("sub") + 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.sync(validated_token, refresh_token) - request.session[SESSION_COOKIE_NAME] = user.session + user.sync(validated_session, refresh_token) + request.session[SESSION_COOKIE_NAME] = user.session_token["jwt"] return user return None diff --git a/django_descope/models.py b/django_descope/models.py index a1c901c..0647d84 100644 --- a/django_descope/models.py +++ b/django_descope/models.py @@ -1,11 +1,11 @@ import logging -from descope import DescopeClient +from descope import SESSION_TOKEN_NAME from django.contrib.auth import models as auth_models from django.core.cache import cache -from django.utils.functional import cached_property -from .settings import IS_STAFF_ROLE, IS_SUPERUSER_ROLE, PROJECT_ID +from . import descope_client +from .settings import IS_STAFF_ROLE, IS_SUPERUSER_ROLE logger = logging.getLogger(__name__) @@ -14,35 +14,32 @@ class DescopeUser(auth_models.User): class Meta: proxy = True - # User is always active since Descioe will never issue a token for an + # User is always active since Descope will never issue a token for an # inactive user is_active = True - _descope = DescopeClient(PROJECT_ID) def sync(self, session, refresh): - self.token = session - self.session = session.get("jwt") - self.refresh = refresh - self.user = session.get("user") - self.firstSeen = session.get("firstSeen") + self.session_token = session[SESSION_TOKEN_NAME] # this should always exist + self.refresh_token = refresh self.username = self._me.get("userId") + self.user = self.username self.email = self._me.get("email") - self.is_staff = IS_STAFF_ROLE in self._roles - self.is_superuser = IS_SUPERUSER_ROLE in self._roles + self.is_staff = descope_client.validate_roles( + self.session_token, [IS_STAFF_ROLE] + ) + self.is_superuser = descope_client.validate_roles( + self.session_token, [IS_SUPERUSER_ROLE] + ) self.save() def __str__(self): return f"DescopeUser {self.username}" - @cached_property + @property def _me(self): return cache.get_or_set( - f"descope_me:{self.username}", lambda: self._descope.me(self.refresh) + f"descope_me:{self.username}", lambda: descope_client.me(self.refresh_token) ) - @cached_property - def _roles(self): - return self.token.get("roles", []) - def get_username(self): return self.username diff --git a/django_descope/settings.py b/django_descope/settings.py index c2353f6..a24c6ad 100644 --- a/django_descope/settings.py +++ b/django_descope/settings.py @@ -5,6 +5,7 @@ settings, "DESCOPE_WEB_COMPONENT_SRC", "https://unpkg.com/@descope/web-component" ) +MANAGEMENT_KEY = getattr(settings, "DESCOPE_MANAGEMENT_KEY", None) PROJECT_ID = getattr(settings, "DESCOPE_PROJECT_ID", None) if not PROJECT_ID: raise ImproperlyConfigured('"DESCOPE_PROJECT_ID" is required!') diff --git a/django_descope/views.py b/django_descope/views.py index cf65bb5..748f760 100644 --- a/django_descope/views.py +++ b/django_descope/views.py @@ -1,18 +1,14 @@ import logging -from descope import REFRESH_SESSION_COOKIE_NAME, SESSION_COOKIE_NAME, DescopeClient +from descope import REFRESH_SESSION_COOKIE_NAME, SESSION_COOKIE_NAME from django.http import HttpRequest, HttpResponseBadRequest, JsonResponse from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.cache import never_cache -from . import settings - # User = get_user_model() logger = logging.getLogger(__name__) -descope_client = DescopeClient(project_id=settings.PROJECT_ID) - @method_decorator([never_cache], name="dispatch") class StoreJwt(View): diff --git a/example_app/templates/descope_login.html b/example_app/templates/descope_login.html index 7ff0b47..2ce29cf 100644 --- a/example_app/templates/descope_login.html +++ b/example_app/templates/descope_login.html @@ -12,6 +12,7 @@ {% if user.is_authenticated %}

Welcome {{ user.email }} you are logged in!

+

Detailed user information

Log Out

{% else %} {% descope_flow "sign-up-or-in" "/" %} diff --git a/example_app/urls.py b/example_app/urls.py index 52225bf..848b22a 100644 --- a/example_app/urls.py +++ b/example_app/urls.py @@ -1,9 +1,9 @@ from django.urls import path from django.views.generic import TemplateView -from .views import Index +from .views import Debug urlpatterns = [ path("", TemplateView.as_view(template_name="descope_login.html"), name="index"), - path("test", Index.as_view(), name="test"), + path("debug", Debug.as_view(), name="debug"), ] diff --git a/example_app/views.py b/example_app/views.py index 8f54022..30f4e3b 100644 --- a/example_app/views.py +++ b/example_app/views.py @@ -5,6 +5,9 @@ from django.urls import reverse from django.views import View +from django_descope import descope_client +from django_descope.models import DescopeUser + logger = logging.getLogger(__name__) @@ -14,9 +17,16 @@ def get(self, request: HttpRequest): return HttpResponseRedirect(reverse("index")) -class Index(View): +class Debug(View): def get(self, request: HttpRequest): - logger.info("Index view called") + logger.info("Debug view called") + mgmt = False + try: + descope_client.mgmt + mgmt = True + except Exception: + pass + return JsonResponse( { "user": request.user.username, @@ -24,5 +34,13 @@ def get(self, request: HttpRequest): "is_staff": request.user.is_staff, "is_superuser": request.user.is_superuser, "email": request.user.email, + "session": request.user.session_token, + "is_mgmt_available": mgmt, + } + if isinstance(request.user, DescopeUser) + else { + "is_authenticated": request.user.is_authenticated, + "is_anonymous": request.user.is_anonymous, + "is_active": request.user.is_active, } )