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 Utilizer
model (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: