# A Deep Dive into the Descope Django Plugin

[Eric Sheen](/content/blog/author/eric-sheen/index.html)

DevRel Engineering Intern

In this tutorial, we will cover in detail how to integrate Descope authentication into your app using the Django Plugin.

To see the django-descope plugin in action, check out [this tutorial](/content/blog/post/auth-django-app/index.html) where we create a sample text summarizer app with full authentication and RBAC.

## Prerequisites

All the code for the tutorial will be in the GitHub Repository at [django-descope](https://github.com/descope/django-descope). Instructions on the installation are on the README.md file.

## Overview

To break down the django-descope plugin, we will go over six parts:

1. Settings
2. URLs + Views
3. Descope.py
4. Models
5. Authentication
6. Middleware

## Settings

Upon cloning the repository, you will see `django_descope`, `example_app`, and `static` folder. `django_descope` is our main plugin, while `example_app` is the app that was created to demonstrate the functionality of the `django_descope` plugin.

Let’s first view our `settings.py` file.

```python
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured

WEB_COMPONENT_SRC = getattr(
    settings, "DESCOPE_WEB_COMPONENT_SRC", "https://unpkg.com/@descope/web-component"
)

PROJECT_ID = getattr(settings, "DESCOPE_PROJECT_ID", None)
if not PROJECT_ID:
    raise ImproperlyConfigured('"DESCOPE_PROJECT_ID" is required!')

# Role names to create in Descope that will map to User attributes
IS_STAFF_ROLE = getattr(settings, "DESCOPE_IS_STAFF_ROLE", "is_staff")
IS_SUPERUSER_ROLE = getattr(settings, "DESCOPE_IS_SUPERUSER_ROLE", "is_superuser")
```

_Code block: settings.py_

- First, we import our global settings into the `django_descope` app project settings.

- The `WEB_COMPONENT_SRC` variable gets the attribute from our global settings file. The purpose of the `WEB_COMPONENT_SRC` is to allow for the functionality of Descope's HTML login widget.

- `PROJECT_ID` works similarly. We get the `DESCOPE_PROJECT_ID` from the settings file.

- `IS_STAFF_ROLE` and `IS_SUPERUSER_ROLE` also look for the attributes in the settings folder and set the default values if they’re not found.

## URLs + Views

Now let’s make our way to the `urls.py` file. There's only one path, `store_jwt`, which calls the `StoreJwt` view in our `views.py` file.

```python
import logging

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

# User = get_user_model()
logger = logging.getLogger(__name__)

@method_decorator([never_cache], name="dispatch")
class StoreJwt(View):
    def post(self, request: HttpRequest):
        session = request.POST.get(SESSION_COOKIE_NAME)
        refresh = request.COOKIES.get(REFRESH_SESSION_COOKIE_NAME)
        if not refresh:
            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
            return JsonResponse({"success": True})

return HttpResponseBadRequest()
```

_Code block: views.py file_

Starting at the top, we import our Descope dependencies using the [Descope Python SDK](https://github.com/descope/python-sdk). Here’s what happens next:

1. The `StoreJwt` class view is wrapped in Django’s `never_cache` method decorator. The `never_cache` decorator tells Django to never cache the route, meaning we do not store / cache the JWT in the browser. Nothing is saved.

2. The post method within our `StoreJwt` class handles the post requests. In the post method, we get our session and refresh tokens from the cookie in the request. Every time a request is made, cookies are sent in the request. The if-statement checks if a refresh cookie exists, else we get it from the POST request.

3. The last if statement now sets our Django session to the cookie values. The session persists across requests, so the cookie values must match the session values. Every time a request is made, Django’s session middleware gets the incoming request before it reaches our routes in the `urls.py`, grabs our cookies in the request, and sets the proper session values.

## Descope.py

Within the `django_descope` plugin, open the `templatetags` folder and open `descope.py`. The custom template tag renders the Descope authentication component.

```python
import os

from descope import REFRESH_SESSION_COOKIE_NAME, SESSION_COOKIE_NAME
from django import template
from django.middleware.csrf import get_token as csrf_token
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.safestring import mark_safe

from ..settings import PROJECT_ID, WEB_COMPONENT_SRC
```

_Code block: descope\_flow imports_

At the top, we import our `PROJECT_ID` and `WEB_COMPONENT_SRC` variables from our settings file. Also notice that we import template from Django.

> NOTE: the WEB\_COMPONENT\_SRC gives us access to the functionality from [Descope’s web.js package](https://github.com/descope/descope-js).

Now let’s see how we initialize our template tags.

```python
register = template.Library()
CONTEXT_KEY = "descope_wc_included"

@register.simple_tag(takes_context=True)
```

_Code block: descope\_flow decorator_

An instance of the template Library is created to register our module. Then we have our `descope_flow` function/template that’s wrapped in a `simple_tag` decorator. This tells Django that the `descope_flow` is our custom template tag and allows us to accept any number of parameters.

Notice how within the `simple_tags` there is `take_context=True`. This allows Django to get the current context data of the HTML template.

Now let’s look at the `descope_flow` method and its parameters.

```python
def descope_flow(context, flow_id, success_redirect):
```

_Code block: descope\_flow parameters_

Let’s take a deeper look at our parameters.

The parameters are `context`, `flow_id`, and `success_redirect`. These three arguments must be present in the `descope_flow` template tags when we use them in the HTML templates. So what are these?

1. **context:** Since we set the `take_context` to true, we must have the context as the first parameter. We can now access the data of the template.

2. **flow\_id:** The `flow_id` is unique to Descope and determines the authentication flow. Ex: sign-up, sign-up-or-in, etc.

3. **success\_redirect:** This is the URL we will be redirected to upon successful login.

Now let’s look at the rest of the code.

```python
  script = ""
    if not context.get(CONTEXT_KEY):
        script += f'<script src="{WEB_COMPONENT_SRC}"></script>'
        context[CONTEXT_KEY] = True
    id = "descope-" + get_random_string(length=4)
    store_jwt_url = reverse("django_descope:store_jwt")
    flow = f"""
    <descope-wc id="{id}" project-id="{PROJECT_ID}" flow-id="{flow_id}"
        base-url="{os.environ.get('DESCOPE_BASE_URI', '')}"></descope-wc>
    <script>
        const descopeWcEle = document.getElementById('{id}');
        descopeWcEle.addEventListener('success', async (e) => {{
            const formData = new FormData();
            formData.append('{SESSION_COOKIE_NAME}', e.detail.sessionJwt);
            formData.append('{REFRESH_SESSION_COOKIE_NAME}', e.detail.refreshJwt);
            formData.append('csrfmiddlewaretoken','{csrf_token(context.request)}')

await fetch("{store_jwt_url}", {{
                method: "POST",
                body: formData,
            }})
            document.location.replace("{success_redirect}")
        }});
    </script>
    """
    return mark_safe(script + flow)
```

_Code block: descope\_flow template_

Here’s the breakdown:

- We have a `script` variable which is set to an empty string for now. There are also the `CONTEXT_KEY` and `WEB_COMPONENT_SRC` variables.

- We need an ID for our descope component so we have an id variable where we call the `get_random_string` method that is inbuilt and imported from Django. The `get_random_string` method generates a random alphanumeric string.

- The `store_jwt_url` variable stores our route which is in our urls.py paths.

- The `flow` string variable stores the actual Descope JavaScript widget that will be rendered in the HTML template. It contains our ID, Project ID, and Flow ID.

- The `form_data` variable creates a new `FormData` object. The `FormData` object is like an HTML form but in JavaScript. It allows us to append key-value pair values so that when we fetch from the `store_jwt_url`, we can pass in the `FormData` object in the body. The session and refresh JWTs along with the CSRF token are sent in the `FormData` object.

- We then return the entire string wrapped in `mark_safe`, which tells Django that it’s safe to be rendered as output in HTML.

## Models

Models in Django are an important part of handling data and our authentication because it allows us to modify the data schema.

```python
import logging

from descope import SESSION_TOKEN_NAME
from django.contrib.auth import models as auth_models
from django.core.cache import cache

from . import descope_client
from .settings import IS_STAFF_ROLE, IS_SUPERUSER_ROLE
```

_Code block: Model imports_

We import the django auth models and we also import our `IS_STAFF_ROLE`, `IS_SUPERUSER_ROLE` and `PROJECT_ID` from our global project settings file.

```python
logger = logging.getLogger(__name__)

class DescopeUser(auth_models.User):
    class Meta:
        proxy = True

# User is always active since Descope will never issue a token for an
    # inactive user
    is_active = True

def sync(self, session, refresh):
        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 = 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}"

@property
    def _me(self):
        return cache.get_or_set(
            f"descope_me:{self.username}", lambda: descope_client.me(self.refresh_token)
        )

def get_username(self):
        return self.username
```

_Code block: models.py file_

The `DescopeUser` model inherits the attributes from Django's default auth\_models user model. The meta class within our model then allows us to add different properties to our model. Setting the proxy to true allows us to manipulate the properties of the model as well.

We then have the `sync` function which is the constructor of the class; it takes in the session and refresh tokens as arguments.

In the \_me method, we return `cache.get_or_set` where we can set key-value pairs to the cache. We have the `descope_me` set to our username and a lambda function that calls `_descope.me` with the refresh token. The `_descope.me` is from Descope and allows us to get user information such as name and email.

## Authentication.py

Our `authentication.py` file handles the validation and authentication of the user.

```python
import logging

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

logger = logging.getLogger(__name__)

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)

logger.debug("Validating (and refreshing) Descope session")
        try:
            validated_session = descope_client.validate_and_refresh_session(
                session_token, refresh_token
            )

except AuthException as e:
            logger.debug(e)
            logout(request)
            return None

if settings.DEBUG:
            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.sync(validated_session, refresh_token)
            request.session[SESSION_COOKIE_NAME] = user.session_token["jwt"]
            return user
        return None
```

_Code block: authentication.py file_

In our authentication.py file, `descope_client` is imported from our `init` file. We also have our `DescopeAuthentication` Backend class. Since we’re using a custom authentication backend, the `BaseBackend` class is inherited, which is Django’s default way of handling permissions and user management.

Here’s the breakdown:

- The `super()` in the `init` inherits the properties of the `BaseBackend` class.

- In the authenticate method, we validate the session and refresh tokens which we get from the session in the request. In the try block, we try validating the session and refresh token using the `descope_client.validate_and_refresh_session` method.

- In the `get_user` method, we take in the validated session and refresh token as arguments and use them to get our user information.

## Middleware.py

The middleware ties everything together.

```python
import logging

from django.contrib.auth import login
from django.http import HttpRequest, HttpResponse

from .authentication import DescopeAuthentication

logger = logging.getLogger(__name__)

class DescopeMiddleware:
    _auth = DescopeAuthentication()

def __init__(self, get_response: HttpResponse = None):
        self.get_response = get_response

def __call__(self, request: HttpRequest):
        user = self._auth.authenticate(request)
        if user:
            login(request, user)
        return self.get_response(request)
```

_Code block: middleware.py file_

Every middleware has two important methods – the `init` and the `call`.

- Our `init` must accept the `get_response` parameter to initialize the middleware only once when the server starts.

- The `call` method is called every time a request is made. In the call method, we use our `DescopeAuthentication` authenticate function to authenticate the request.

## Congrats!

Whew! That was a lot to take in but hopefully you learned something new about Django authentication or Descope!

To see the Descope Django plugin in action, check out [this tutorial](/content/blog/post/auth-django-app/index.html) where we build a sample app with full authentication and custom admin login.

If reading this tutorial made you curious to try Descope, [sign up for a Free Forever account](/content/sign-up/index.html) and join [AuthTown](/content/community/index.html), our open user community for developers looking to learn about authentication.
