Hvordan integrere Azure Entra ID med Django

Vi fikk nylig en forespørsel fra en bruker av Mist om vi kunne legge til støtte for innlogging gjennom Microsoft Entra ID (tidligere kjent som Azure Active Directory). Entra ID er en av de mest brukte skybaserte autentiseringsløsningene på markedet, og er ofte foretrukket av bedrifter som allerede er integrert mot andre Microsoft-produkter. Den gjør det blant annet mulig å sette opp tilgangsstyring mot tredjepartsapplikasjoner ved å bruke bedriftens eget tilgangshierarki, noe som kan bidra til å minimere risikoen for datainnbrudd.

Ettersom Mist er en Django-applikasjon, har vi siden start kunnet dra nytte av det robuste og utvidbare autentiseringssystemet som kommer med ut av boksen. Dette, kombinert med positive resultater fra forundersøkelser, gjorde oss sikre på at en slik integrasjon var mulig å få til, så vi takket vi ja til denne utfordringen!

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:

Skrevet av
Christian De Frène

Andre artikler