Skip to content

Commit

Permalink
feat: expose descope sdk (descope#153)
Browse files Browse the repository at this point in the history
## Related Issues

Fixes descope#152

## Description

* Add an optional setting to allow using [Descope SDK Management
API](https://docs.descope.com/manage/)
* Expose a reusable `descope` client on top level of module
* Fix role validation to use Descope SDK
  • Loading branch information
omercnet authored Jun 13, 2023
1 parent 8a99cc7 commit 81b6794
Show file tree
Hide file tree
Showing 9 changed files with 73 additions and 45 deletions.
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
7 changes: 7 additions & 0 deletions django_descope/__init__.py
Original file line number Diff line number Diff line change
@@ -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]
34 changes: 17 additions & 17 deletions django_descope/authentication.py
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
33 changes: 15 additions & 18 deletions django_descope/models.py
Original file line number Diff line number Diff line change
@@ -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__)

Expand All @@ -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
1 change: 1 addition & 0 deletions django_descope/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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!')
Expand Down
6 changes: 1 addition & 5 deletions django_descope/views.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
1 change: 1 addition & 0 deletions example_app/templates/descope_login.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<body>
{% if user.is_authenticated %}
<h1>Welcome {{ user.email }} you are logged in!</h1>
<p><a href="{% url 'debug' %}">Detailed user information</a></p>
<p><a href="{% url 'logout' %}">Log Out</a></p>
{% else %}
{% descope_flow "sign-up-or-in" "/" %}
Expand Down
4 changes: 2 additions & 2 deletions example_app/urls.py
Original file line number Diff line number Diff line change
@@ -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"),
]
22 changes: 20 additions & 2 deletions example_app/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)


Expand All @@ -14,15 +17,30 @@ 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,
"is_authenticated": request.user.is_authenticated,
"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,
}
)

0 comments on commit 81b6794

Please sign in to comment.