How to integrate Azure Entra ID with Django

We recently received a request from a user of Mist if we could add support for login through Microsoft Entra ID (formerly known as Azure Active Directory). Entra ID is one of the most widely used cloud-based authentication solutions on the market, and is often preferred by businesses that are already integrated against other Microsoft products. Among other things, it enables access management to be set up against third-party applications using the company's own access hierarchy, which can help minimize the risk of data breaches.

As Mist is a Django application, since its inception we have been able to take advantage of the robust and extensible authentication system that comes with out of the box. This, combined with positive results from preliminary investigations, made us confident that such integration was possible, so we accepted this challenge!

Technical architecture

Mist is a web application that consists of a backend written in Django, a frontend written in React, and a communication layer based on GraphQL. To integrate with Entra ID, we therefore need to start the authentication session on the client side (frontend) so that the user can enter login details, and then transfer the session to the server side (backend) to check application-level accesses. This article is based on this architecture.

The actual interaction with Entra ID occurs through either the Microsoft Graph API, or the Microsoft Authentication Library (MSAL). Luckily, there are existing packages that do the heavy lifting for us, so we don't have to code this from scratch!

Guidebook

The integration involves the following steps:

1. Create Entra ID app registratie in the Azure portal

The authentication procedure is based on connecting to an app registration configuration created on your organization in Azure. For architectures with a client and server side, one has to create one app registration for each of them (frontend and backend), though with a few different parameters.

We followed this recipe to configure all the necessary setup: https://django-auth-adfs.readthedocs.io/en/latest/azure_ad_config_guide.html

Once setup is complete, you are left with a set of configuration keys that will be used by the client and server applications in the next step.

In this portal, further access restrictions and adjustments can also be made. A tip we received from the user who asked us to add support is to go to Enterprise-applicaties in the side menu and search up the app registrations one just created. There you can assign users and groups, so that only these have the opportunity to log in to the application.

2. Install and configure the package django-auth-adfs

We took advantage of the package django-auth-adfs to extend Django's authentication system with support for Entra ID.

First we have to enter the env variables AZURE_CLIENT_ID, AZURE_CLIENT_SECRET and AZURE_TENANT_ID which was extracted from the app registration of backend from step 1.

Our installation involved, in practice, adding the following object to settings.py:

# Azure Entra ID
AUTH_ADFS = {
    "AUDIENCE": os.getenv("AZURE_CLIENT_ID"),
    "CLIENT_ID": os.getenv("AZURE_CLIENT_ID"),
    "CLIENT_SECRET": os.getenv("AZURE_CLIENT_SECRET"),
    "TENANT_ID": os.getenv("AZURE_TENANT_ID"),
    "RELYING_PARTY_ID": os.getenv("AZURE_CLIENT_ID"),
    "CLAIM_MAPPING": {
        "first_name": "given_name",
        "last_name": "family_name",
        "email": "upn",
    },
}

CLAIM_MAPPING controls how user data from Entra ID (right side) is passed on to Django's Utilizermodel (left side). This is a minimal example; one can choose for himself how much or little data to extract.

3. Install client-specific packages

This step depends on what technology you use on the frontend of your application. For Mist, which uses React, we needed to install the packages @azure /msal-browser and @azure /msal-react.

First, enter the env variables AZURE_CLIENT_ID, AZURE_TENANT_ID which was extracted from the app registration of frontend from step 1, and AZURE_BACKEND_CLIENT_ID which was extracted from the app registration of backend. Remember that these must be exposed to the browser; in our case we use the prefix REACT_APP_ to this.

Then you create a MSALConfig object that handles the SSO login:

import { type Configuration, PublicClientApplication } from '@azure/msal-browser'

const msalConfig: Configuration = {
  auth: {
    clientId: `${process.env.REACT_APP_AZURE_CLIENT_ID}`,
    authority: `https://login.microsoftonline.com/${process.env.REACT_APP_AZURE_TENANT_ID}`,
    redirectUri: window.location.origin,
  },
  cache: {
    cacheLocation: 'localStorage',
    storeAuthStateInCookie: false,
  },
}

const msalInstance = new PublicClientApplication(msalConfig)

export default msalInstance

Further, we made a <SignInWithMicrosoft />component that can be used on the login page:

import React from 'react'
import { useMsal } from '@azure/msal-react'

export const SignInWithMicrosoft: React.FC = () => {
  const { instance } = useMsal()

  function handleSignInWithMicrosoft() {
    instance
      .loginPopup({
        scopes: [`${process.env.REACT_APP_AZURE_BACKEND_CLIENT_ID}/.default`],
      })
      .then(handleLogin)
      .catch(handleLoginFailure)
  }

  return (
    <img
      src="https://learn.microsoft.com/en-us/entra/identity-platform/media/howto-add-branding-in-apps/ms-symbollockup_signin_light.svg"
      alt="Sign in with Microsoft"
      onClick={handleSignInWithMicrosoft}
    />
  )
}

HandelsLogin and HandleLoginFailure is used to pass the token from Entra ID on to our backend, where it can be validated and used by business logic.

4. Customize existing business logins

In Mist, we already support token-based authentication at login with username and password, and we wanted to keep this in parallel with the SSO login. Fortunately, it is easy to support both by creating a proprietary middleware that extends the existing business logic.

Here's a simplified example:

from django_auth_adfs.backend import AdfsAccessTokenBackend

class MsalProviderMiddleware:
    def __call__(self, request):
        self.authorize_token(request)
        return request

    def authorize_token(self, request):
        # Extract token from the request
        token = request.token
        
        # Ensure token was issued by Entra ID, skip otherwise.
        # This is done so we don't try to decode our self-issued tokens.
        unverified_token = jwt.decode(token, verify=False)
        if not unverified_token["iss"].startswith("https://sts.windows.net"):
            return
            
        try:
            # django-auth-adfs will automatically verify the token 
            # using official Entra ID channels, before the user 
            # data is returned.
            user = AdfsAccessTokenBackend.authenticate(
                request=request, access_token=token.encode("utf-8")
            )
            
            request.user = user
            request.success = True
            
        except PermissionDenied:
            # Handle failed login
                ...

Then add the new class into the middleware list in settings.py:

MIDDLEWARE = [
    ...
    "path.to.MsalProviderMiddleware",
    ...
]

Now we have a new button on the login screen that can be used to log in to Mist using Entra ID:

Summarized

If you are in a similar situation to us, we can safely say that such an integration is both possible to add (even in an existing Django application) and is also not particularly difficult or time-consuming to perform.

It is, of course, possible to tailor the configuration beyond the examples in this article. Here I recommend browsing the documentation of the packages used to get an overview of the options available:

Skrevet av
Christian De Frène

Andre artikler