Teknisk arkitektur
Mist er en webapplikasjon som består av en backend skrevet i Django, en frontend skrevet i React, og et kommunikasjonslag som baserer seg på GraphQL. For å integrere mot Entra ID er vi derfor nødt til å starte autentiseringsøkten på klientsiden (frontend) slik at bruker kan oppgi innloggingsdetaljer, for deretter å overføre økten til tjenersiden (backend) for å sjekke tilganger på applikasjonsnivå. Denne artikkelen tar utgangspunkt i denne arkitekturen.
Selve interaksjonen med Entra ID skjer gjennom enten Microsoft Graph APIet, eller Microsoft Authentication Library (MSAL). Heldigvis finnes det eksisterende pakker som gjør de tunge løftene for oss, så vi slipper å kode dette fra bunnen av!
Guide
Integrasjonen innebærer følgende steg:
1. Lag Entra ID app registrations i Azure-portalen
Autentiseringsprosedyren baserer seg på at man kobler seg mot en konfigurasjon (app registration) som er opprettet på din organisasjon i Azure. For arkitekturer med en klient- og tjenerside er man nødt til å opprette én app registration for hver av dem (frontend og backend), dog med noen ulike parametre.
Vi fulgte denne oppskriften for å konfigurere alt nødvendig oppsett: https://django-auth-adfs.readthedocs.io/en/latest/azure_ad_config_guide.html
Når oppsettet er fullført sitter man igjen med et sett konfigurasjonsnøkler som skal brukes av klient- og tjenerapplikasjonene i neste steg.
I denne portalen kan man også gjøre videre tilgangsbegrensninger og -tilpasninger. Et tips vi mottok fra brukeren som ba oss legge til støtte, er å gå til Enterprise Applications i sidemenyen og søke opp appregistreringene man akkurat opprettet. Der kan man tilegne brukere og grupper, slik at kun disse har mulighet til å logge inn på applikasjonen.
2. Installer og konfigurer pakken django-auth-adfs
Vi benyttet oss av pakken django-auth-adfs for å utvide Django’s autentiseringssystem med støtte for Entra ID.
Først er vi nødt til å legge inn env-variablene AZURE_CLIENT_ID
, AZURE_CLIENT_SECRET
og AZURE_TENANT_ID
som ble hentet ut fra appregistreringen til backend fra steg 1.
Vår installasjon innebar i praksis å legge til følgende objekt i 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
styrer hvordan brukerdata fra Entra ID (høyre side) sendes videre til Django’s User
-modell (venstre side). Dette er et minimalt eksempel; man kan selv velge hvor mye eller lite data som skal hentes ut.
3. Installer klient-spesifikke pakker
Dette steget avhenger av hvilken teknologi du bruker på frontend av applikasjonen din. For Mist, som bruker React, trengte vi installere pakkene @azure/msal-browser
og @azure/msal-react
.
Først legger man inn env-variablene AZURE_CLIENT_ID
, AZURE_TENANT_ID
som ble hentet ut fra appregistreringen til frontend fra steg 1, samt AZURE_BACKEND_CLIENT_ID
som ble hentet ut fra appregistreringen til backend. Husk at disse må være eksponert til nettleseren; i vårt tilfelle bruker vi prefikset REACT_APP_
til dette.
Deretter lager man et msalConfig
objekt som håndterer SSO-innloggingen:
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
Videre lagde vi en <SignInWithMicrosoft />
-komponent som kan brukes på login-siden:
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}
/>
)
}
handleLogin
og handleLoginFailure
brukes for å sende tokenet fra Entra ID videre til vår backend, der det kan valideres og brukes av business-logikken.
4. Tilpass eksisterende business-logikk for innlogging
I Mist støtter vi allerede token-basert autentisering ved innlogging med brukernavn og passord, og vi ønsket å beholde denne i parallell med SSO-innloggingen. Det er heldigvis enkelt å støtte begge deler ved å lage en egen middleware som utvider den eksisterende business-logikken.
Her er et forenklet eksempel:
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
...
Legg deretter den nye klassen inn i middleware-listen i settings.py
:
MIDDLEWARE = [
...
"path.to.MsalProviderMiddleware",
...
]
Nå har vi en ny knapp på login-skjermen som kan brukes til å logge inn på Mist ved hjelp av Entra ID:
Oppsummert
Er du i en lignende situasjon som oss, kan vi trygt si at en slik integrasjon både er mulig å legge til (selv i en eksisterende Django-applikasjon) og heller ikke er særlig vanskelig eller tidkrevende å utføre.
Det er selvsagt mulig å skreddersy konfigurasjonen utover eksemplene i denne artikkelen. Her anbefaler jeg å bla gjennom dokumentasjonen til pakkene som ble benyttet, for å få en oversikt over hvilke valgmuligheter som finnes: