Cet article est également disponible en anglais.
Read in English

Comment configurer le SSO Azure dans FastAPI avec Microsoft Entra ID

17 min de lecture
Comment configurer le SSO Azure dans FastAPI avec Microsoft Entra ID
Découvrez comment ajouter le SSO Microsoft Entra ID (anciennement Azure AD) à une application FastAPI avec MSAL. Inclut l'enregistrement de l'application, le callback de connexion, les cookies de session, les routes protégées, les rôles et les erreurs courantes.

Dans ce guide complet, nous allons parcourir l’ensemble du processus de configuration de l’authentification unique Azure (SSO) avec FastAPI, depuis la configuration de votre application Microsoft Entra ID jusqu’à l’implémentation du flux d’authentification dans votre application FastAPI.

Remarque : Microsoft a renommé Azure Active Directory (Azure AD) en Microsoft Entra ID. Ce guide utilisera la nouvelle terminologie, mais vous pouvez encore rencontrer les anciens noms dans certaines documentations et articles en ligne.

Résumé du flux d'authentification Microsoft Entra ID

Avant de plonger dans l’implémentation, il est important de comprendre comment fonctionne l’ensemble du processus d’authentification.

  1. Un utilisateur tente d’accéder à votre application FastAPI.
  2. Il est redirigé vers la page de connexion de Microsoft.
  3. Après une authentification réussie, Microsoft Entra ID renvoie un code d’autorisation à votre application.
  4. Votre application échange ce code contre un jeton d'accès contenant les informations de l'utilisateur.
  5. Stockez le jeton dans un cookie de session signé afin que l'utilisateur n'ait à se connecter qu'une seule fois.

Diagramme du processus de connexion avec Azure SSO et FastAPI
Diagramme du processus de connexion SSO

La beauté de cette approche réside dans le fait que votre application ne gère jamais directement les mots de passe des utilisateurs. Toute l’authentification est prise en charge par l’infrastructure sécurisée de Microsoft, et vous vous contentez de valider les jetons qu’ils fournissent. De plus, si l'utilisateur est déjà connecté avec Microsoft, ces redirections peuvent se produire de manière presque invisible.

Ce que nous allons construire

Nous allons construire une petite application FastAPI avec :

  • /login : redirige l'utilisateur vers Microsoft Entra ID
  • /login/callback : reçoit le code d'autorisation de Microsoft Entra ID
  • /protected : une route qui nécessite que l'utilisateur soit connecté
  • /roleProtected : une route qui nécessite que l'utilisateur ait le rôle Admin
  • /unprotected : une route publique
  • SessionMiddleware pour la gestion des cookies de session SSO

Nous allons configurer l'infrastructure Azure comprenant :

  • Enregistrement d'application : pour donner une identité à notre application
  • URL de redirection : pour informer Azure de se connecter à /login/callback après la connexion

Prérequis

Pour suivre ce tutoriel, vous avez besoin de :

  • Python 3.10+
  • Un compte Microsoft Azure
  • Accès à Microsoft Entra ID
  • Connaissances de base de FastAPI
  • Un projet FastAPI local

Étape 1 : Configuration d’Azure

1.1 Créer un enregistrement d'application Azure

Tout d’abord, nous devons configurer un enregistrement d’application avant de mettre en place l’authentification unique (SSO) sur notre application. En environnement de production, il est recommandé de réaliser cette étape à l’aide de pipelines IaC (Infrastructure-as-Code), mais pour ce tutoriel, nous allons le configurer manuellement pour simplifier les choses.

L’enregistrement de l’application sera l’identité de votre application dans Azure. Il peut être utilisé à diverses fins, telles que l’accès basé sur les rôles aux ressources Azure. Ici, nous l’utiliserons uniquement pour l’authentification unique (SSO). Sélectionnez l’enregistrement d’application dans le Portail Azure et cliquez sur « Enregistrer une application ».

Le logo d’enregistrement de l’application dans Azure

Pour créer votre application, il vous suffit de choisir un nom et de sélectionner le type de comptes pris en charge. Cela est assez explicite, comme vous pouvez le voir ci-dessous.

Capture d'écran montrant comment créer un nouvel enregistrement d'application sur Azure

L’URI de redirection est la partie la plus intéressante. Ici, vous allez définir l’URL qu’Azure sera autorisé à utiliser après le processus de connexion. Il s’agit d’une mesure de sécurité utilisée par Azure pour garantir que vos jetons ne puissent pas être volés lors d’une attaque de type « homme du milieu » vers une autre adresse.

Le type d’URI de redirection pour ce tutoriel sera Web, qui est la solution générique pour une API web.

Après cela, vous pouvez alors créer l’enregistrement de votre application.

1.2 Optionnel : Ajouter d’autres URL de redirection

Remarque : Après la création, il sera possible de répertorier plusieurs URI de redirection. Cela est utile, par exemple, pour indiquer une URL locale pour le développement et une URL de production. Pour le développement local, Microsoft Entra ID autorise le HTTP simple sur localhost, vous pouvez donc ajouter en toute sécurité http://localhost:8000/login/callback comme URI de redirection. Il suffit d’aller dans l’onglet d’authentification et de cliquer sur « Ajouter une URI », d’ajouter une nouvelle URL, puis de cliquer sur « Enregistrer ».

Capture d'écran montrant comment ajouter une nouvelle URL de redirection sur Azure

1.3 Créer un secret

Ok, nous avons presque terminé, il ne nous reste plus qu’à créer un secret client. Celui-ci servira de mot de passe pour utiliser l’enregistrement de l’application dans notre application.

  1. Cliquez sur l’onglet « Certificats et secrets »
  2. Dans la section Secrets client, cliquez sur « + Nouveau secret client ».
  3. Saisissez une description (par exemple, clé d’accès API) et choisissez une période d’expiration
  4. Cliquez sur « Ajouter ».
  5. Copiez la valeur immédiatement — elle ne sera affichée qu’une seule fois.

Capture d’écran montrant comment créer un nouveau secret client sur Azure

Encore une fois, en pratique, vous feriez cela en utilisant un type de pipeline IaC et vous stockeriez le secret dans un coffre de clés après l’avoir créé automatiquement. De plus, les secrets clients doivent être renouvelés, donc n’oubliez pas de planifier un moment pour les renouveler ou de mettre en place un processus de renouvellement automatique via IaC.

1.4 Récupérer l’ID client et l’ID de locataire

Go to the overview page and get the Client ID and Tenant ID (also called Directory ID). You now have the 3 strings that we will need to connect to Azure.

1.5 Créer des rôles d'application

Pour utiliser le contrôle d'accès basé sur les rôles (RBAC) dans votre application FastAPI, vous devrez définir ces rôles dans Microsoft Entra ID.

  1. Allez dans votre Enregistrement d'application dans le portail Azure.
  2. Sélectionnez Rôles d'application dans le menu de gauche.
  3. Cliquez sur + Créer un rôle d'application.
  4. Définissez le Nom d'affichage sur "Admin", la Valeur sur "Admin", et choisissez qui peut se voir attribuer ce rôle (par exemple, Utilisateurs/Groupes).
  5. Appliquez le rôle et assurez-vous qu'il est activé.

Lorsqu'un utilisateur avec ce rôle attribué se connecte, le jeton ID inclura une revendication roles contenant "Admin" comme nous le verrons dans la section implémentation.

Étape 2 : Configuration du projet et dépendances

Voici la structure du projet que nous allons construire :

azure-sso-fastapi/
├── main.py
├── requirements.txt
└── .env

Très bien, maintenant, configurons nos dépendances. Commencez par créer un fichier requirements.txt avec le contenu suivant :

    fastapi
    httpx
    python-dotenv
    python-jose # Utilisé pour la validation JWT
    msal # Utilisé pour l'authentification Microsoft
    uvicorn

Ensuite, installez-les en utilisant pip :

    pip install -r requirements.txt

Étape 3 : Implémenter le SSO Azure dans FastAPI

Il est temps de coder ! Nous allons créer un seul fichier main.py pour ce tutoriel. En pratique, vous pourriez vouloir séparer le code d’authentification et les routes dans un fichier différent en utilisant la fonctionnalité de routeur de FastAPI, mais cela dépasse le cadre de cet article.

3.1 Importations et initialisations importantes

Tout d’abord, nous allons devoir importer un certain nombre d’éléments :

import httpx
import secrets
import os
from jose import jwt, JWTError
from typing import Optional, Dict, Any, List
from datetime import datetime, timedelta
from fastapi import FastAPI, Depends, HTTPException, status, Request
from fastapi.responses import RedirectResponse, JSONResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from dotenv import load_dotenv
import msal
from starlette.middleware.session import SessionMiddleware

Ensuite, nous devrons définir quelques éléments, comme nous pouvons le voir dans le code ci-dessous. Ce qui importe le plus est le msal_client, qui représentera l’enregistrement de l’application dans le code. Notez également que nous ajoutons SessionMiddleware aux middlewares de FastAPI. Cela nous sera utile par la suite.

Attention : Le SessionMiddleware de Starlette stocke les données dans un cookie signé côté client. Les navigateurs limitent la taille des cookies à environ 4 Ko. Évitez de stocker des jetons d'accès bruts ou de grandes quantités de données utilisateur dans la session, sinon le cookie échouera silencieusement à s'enregistrer. Ne stockez que les données d'identité essentielles (comme l'ID utilisateur, le nom et les rôles).

AZURE_CLIENT_ID = os.getenv("AZURE_CLIENT_ID")
AZURE_CLIENT_SECRET = os.getenv("AZURE_CLIENT_SECRET")
AZURE_TENANT_ID = os.getenv("AZURE_TENANT_ID")
AZURE_REDIRECT_URI = os.getenv("AZURE_REDIRECT_URI")
SECRET_KEY = os.getenv("SECRET_KEY")
AUTHORITY = f"https://login.microsoftonline.com/{AZURE_TENANT_ID}"
SCOPE = ["User.Read"] # Ce scope est utilisé pour demander l'accès aux infos utilisateur
 
app = FastAPI()
 
# Ajout du session middleware
app.add_middleware(
    SessionMiddleware,
    secret_key=SECRET_KEY,
    https_only=True, 
    samesite="none"    
)
 
# Initialisation du client MSAL
msal_client = msal.ConfidentialClientApplication(
    client_id=AZURE_CLIENT_ID,
    authority=AUTHORITY,
    client_credential=AZURE_CLIENT_SECRET,
)

Pour le développement local via HTTP simple, changez temporairement le middleware en :

app.add_middleware(
    SessionMiddleware,
    secret_key=SECRET_KEY,
    https_only=False,
    same_site="lax",
)

Enfin, vous devrez définir un certain nombre de variables d'environnement, voici un exemple de fichier .env à remplir :

AZURE_CLIENT_ID=votre-client-id
AZURE_CLIENT_SECRET=votre-client-secret
AZURE_TENANT_ID=votre-tenant-id
AZURE_REDIRECT_URI=https://votre-domaine.com/login/callback
SECRET_KEY=votre-secret-key

3.2 Points de terminaison de connexion et de rappel pour le SSO Azure

Écrivons maintenant les points de terminaison d’autorisation de base qui permettront aux utilisateurs de se connecter en utilisant Microsoft Entra ID (Azure AD) et de gérer le rappel OAuth.

L’itinéraire /login redirigera l’utilisateur vers la page de connexion Microsoft. Après s’être connecté à son compte Microsoft, l’utilisateur sera redirigé vers /login/callback. Le lien exact de rappel a été fourni dans /login avec la variable AZURE_REDIRECT_URI. Si l’utilisateur est déjà connecté à Microsoft, il ne verra même pas la redirection côté front-end.

Dans /login/callback, nous échangeons notre code contre un jeton utilisateur. Encore une fois, il s’agit d’une mesure de sécurité pour éviter d’envoyer le jeton sur Internet. Remarquez que nous stockons ensuite notre jeton dans la session utilisateur. C’est essentiel ; sinon, le jeton n’existera que jusqu’à la prochaine redirection. Sans cela, nous pouvons entrer dans une boucle de redirections infinie.

À la fin, la route de rappel redirige l’utilisateur vers la route protégée. Il s’agit d’une implémentation simplifiée. Une approche plus flexible consiste à enregistrer dans la session l’adresse que l’utilisateur a tenté d’atteindre. Ensuite, il suffit de la récupérer dans le rappel afin de s’assurer que l’on puisse revenir à l’endroit d’où l’on vient.

# --- Points de terminaison API ---
 
@app.get("/login")
async def login(request: Request):
    """
    Initie le flux de connexion Microsoft Entra ID.
    Stocke l'état dans la session.
    """
    state = secrets.token_urlsafe(32)
    request.session["state"] = state
 
    authorization_url = msal_client.get_authorization_request_url(
        scopes=SCOPE,
        state=state,
        redirect_uri=AZURE_REDIRECT_URI
    )
    return RedirectResponse(url=authorization_url)
 
async def get_microsoft_jwks():
    """Récupère les clés publiques utilisées pour signer les jetons."""
    async with httpx.AsyncClient() as client:
        response = await client.get(
            f"https://login.microsoftonline.com/{AZURE_TENANT_ID}/discovery/v2.0/keys"
        )
        return response.json()
 
@app.get("/login/callback")
async def callback(code: str, state: str, request: Request):
    """
    Gère le callback OAuth de Microsoft Entra ID.
    Stocke le jeton et les infos de l'utilisateur dans la session.
    """
    # Vérifier l'état pour prévenir les [attaques CSRF](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html)
    if state != request.session.get("state"):
        raise HTTPException(status_code=400, detail="Paramètre d'état invalide")
 
    request.session.pop("state", None)
 
    token_response = msal_client.acquire_token_by_authorization_code(
        code=code,
        scopes=SCOPE,
        redirect_uri=AZURE_REDIRECT_URI
    )
 
    if "error" in token_response:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail=token_response.get("error_description", "Échec de l'acquisition du jeton")
        )
 
    # Récupérer les clés publiques de Microsoft pour vérifier le jeton
    jwks = await get_microsoft_jwks()
    
    # Obtenir l'en-tête non vérifié pour trouver la clé correcte
    unverified_header = jwt.get_unverified_header(token_response['id_token'])
    rsa_key = {}
    for key in jwks["keys"]:
        if key["kid"] == unverified_header["kid"]:
            rsa_key = {
                "kty": key["kty"],
                "kid": key["kid"],
                "use": key["use"],
                "n": key["n"],
                "e": key["e"]
            }
            break
            
    if not rsa_key:
        raise HTTPException(status_code=401, detail="Jeton invalide : clé publique introuvable")
 
    try:
        # Le [id_token](https://jwt.io/introduction/) contient les revendications de l'utilisateur
        # Note : Nous devons vérifier la signature localement en utilisant la clé publique récupérée.
        # C'est un risque de sécurité de sauter la validation.
        id_token_claims = jwt.decode(
            token_response['id_token'],
            rsa_key,
            algorithms=["RS256"],
            audience=AZURE_CLIENT_ID,
            issuer=f"https://login.microsoftonline.com/{AZURE_TENANT_ID}/v2.0"
        )
    except JWTError as e:
        raise HTTPException(status_code=401, detail=f"Échec de la validation du jeton : {str(e)}")
 
    # Stocker les informations de l'utilisateur dans la session
    request.session["user"] = {
        "id": id_token_claims.get("oid"),
        "name": id_token_claims.get("name"),
        "email": id_token_claims.get("preferred_username"),
        "roles": id_token_claims.get("roles", []),
    }
 
    return RedirectResponse(url="/protected")

3.3 Créer des dépendances FastAPI

Créons quelques fonctions utilitaires. Nous les utiliserons comme dépendances avec la fonctionnalité Depends de FastAPI. Cela permet à FastAPI de résoudre et d’injecter automatiquement les résultats de ces utilitaires là où c’est nécessaire, rendant ainsi votre code plus modulaire et plus propre.

Ces fonctions vont simplement essayer d’obtenir l’objet utilisateur ou les rôles à partir de la session en cours. Si cela n’existe pas, elles lèveront un drapeau d’erreur sous forme d’exception HTTP 302. Celle-ci sera envoyée au navigateur web, qui comprendra qu’il doit effectuer une redirection. Dans ce cas, vers le point de terminaison /login.

# --- Logique d'authentification ---
 
# --- Dépendances ---
def require_auth(request: Request) -> Dict[str, Any]:
    """
    Dépendance pour protéger un point de terminaison.
    Redirige vers /login si l'utilisateur n'est pas authentifié.
    """
    user = request.session.get("user")
    if not user:
        raise HTTPException(
            status_code=status.HTTP_302_FOUND,
            headers={"Location": "/login"}
        )
    return user
 
def require_roles(required_roles: List[str]):
    """
    Fabrique de dépendances pour vérifier les rôles requis dans la session.
    """
    def role_checker(
        user: Dict[str, Any] = Depends(require_auth)
    ):
        user_roles = user.get("roles", [])
        if not any(role in user_roles for role in required_roles):
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Vous n'avez pas la permission d'accéder à cette ressource."
            )
        return user
    return role_checker

3.4 Créons quelques routes protégées réelles avec FastAPI

Parfait, maintenant que nous avons géré la logique d’autorisation, nous pouvons l’utiliser pour proposer des points de terminaison utiles dans nos applications. Voici quelques exemples qui nécessitent un jeton utilisateur, un rôle spécifique, ou aucune connexion du tout. Remarquez comment nous utilisons la fonctionnalité Depends() de FastAPI pour intégrer de manière transparente l’authentification dans nos points de terminaison.

@app.get("/")
async def root():
    return {
        "message": "Exemple FastAPI Microsoft Entra ID SSO",
        "routes": {
            "login": "/login",
            "unprotected": "/unprotected",
            "protected": "/protected",
            "role_protected": "/roleProtected",
        },
    }
 
@app.get("/unprotected")
async def unprotected_endpoint():
    """Route publique."""
    return {"message": "Ceci est un point de terminaison non protégé."}
 
@app.get("/protected")
async def protected_endpoint(
    user: Dict[str, Any] = Depends(require_auth)
):
    """Route protégée requiring login."""
    return {
        "message": f"Bonjour, {user.get('name')}! Voici des données protégées.",
        "user_details": user
    }
 
@app.get("/roleProtected")
async def role_protected_endpoint(
    user: Dict[str, Any] = Depends(require_roles(["Admin"]))
):
    """Route protégée par le rôle 'Admin'."""
    return {
        "message": f"Bienvenue, Admin {user.get('name')}!",
        "detail": "Vous avez accès à ces données protégées par rôle."
    }

Une note importante sur les cookies de session et l’authentification unique (SSO)

Nous utilisons le SessionMiddleware de Starlette pour la gestion des sessions. Lorsqu’un utilisateur se connecte, ses informations d’identité sont stockées dans un cookie sécurisé et signé.

Au cours du processus SSO, l’utilisateur est redirigé de votre application vers la page de connexion de Microsoft, puis de retour. Du point de vue du navigateur, il s’agit d’une requête intersite. Pour que le cookie de session soit correctement géré après cette redirection, il doit être configuré avec SameSite=None et https_only=True. Cela indique au navigateur que le cookie peut être envoyé dans des contextes cross-origin, mais uniquement via HTTPS.

Astuce 1 : Lorsque SameSite=None est utilisé, les navigateurs refuseront silencieusement de définir le cookie si la connexion n’est pas sécurisée (c’est-à-dire, non HTTPS). Cela peut être très difficile à déboguer car votre flux d’authentification échouera simplement sans erreur évidente. Lors du déploiement sur Azure ou dans tout autre environnement de production, assurez-vous d’utiliser HTTPS.

Astuce 2 : Cela peut rendre le travail en local difficile ; vous devrez également utiliser une URL HTTPS pour le développement local. Dans certains cas, certains navigateurs vous permettront de désactiver cette sécurité à des fins de développement.

Astuce 3 : Soyez prudent lors de la construction de vos objets de réponse dans FastAPI. Si vous créez un nouvel objet Response au lieu de laisser FastAPI le gérer, vous risquez d’effacer accidentellement l’en-tête Set-Cookie ajouté par le SessionMiddleware, ce qui déconnectera effectivement l’utilisateur. Il est préférable de modifier request.session et de laisser le middleware gérer la gestion des cookies.

Étape 4 : Tester votre implémentation

Maintenant que tout est en place, testons le flux d’authentification. Démarrez votre application FastAPI :

    python main.py

Ou en utilisant directement uvicorn :

    uvicorn main:app --reload

Votre application devrait maintenant être en cours d’exécution. Vous pouvez tester le processus d’authentification :

  1. Accédez à l’adresse dans votre navigateur
  2. Vous serez redirigé vers la page de connexion de Microsoft
  3. Connectez-vous avec vos identifiants Microsoft Entra ID
  4. Après une authentification réussie, vous serez redirigé vers votre point de terminaison de rappel
  5. Le rappel renverra vos informations utilisateur et vos jetons
  6. Vous devriez maintenant avoir accès à votre application

Étape 5 : Clause de non-responsabilité en matière de sécurité

Bien que l'implémentation ci-dessus couvre le flux SSO complet et inclue la vérification de la signature, ce guide est intentionnellement simplifié à des fins d'apprentissage. Avant de passer en production, considérez les meilleures pratiques de sécurité suivantes :

  1. Utilisez HTTPS partout : Utilisez toujours HTTPS en production. Le SessionMiddleware dépend de cookies sécurisés qui seront rejetés via HTTP simple lors de redirections intersites.
  2. Rotation des secrets : Stockez votre SECRET_KEY et votre AZURE_CLIENT_SECRET de manière sécurisée (par exemple, en utilisant Azure Key Vault) et renouvelez-les régulièrement. Ne les codez pas en dur.
  3. Sessions côté serveur : Pour les applications à haute sécurité ou avec une grande quantité de données utilisateur, envisagez d'utiliser des sessions côté serveur (par exemple, en stockant les données de session dans Redis) au lieu de cookies côté client.
  4. Validation des jetons : Bien que nous ayons ajouté une validation de signature de base, assurez-vous de valider l'émetteur (issuer), l'audience et les revendications d'expiration. Dans une application multi-locataire, vous devez également valider soigneusement l'ID du locataire (tenant ID).

Exemple complet de SSO Azure avec FastAPI

Voici le fichier main.py complet :

import os
import secrets
from typing import Any, Dict, List
 
import httpx
import msal
from dotenv import load_dotenv
from fastapi import Depends, FastAPI, HTTPException, Request, status
from fastapi.responses import RedirectResponse
from jose import jwt, JWTError
from starlette.middleware.session import SessionMiddleware
 
 
load_dotenv()
 
# -------------------------------------------------------------------
# Configuration
# -------------------------------------------------------------------
 
AZURE_CLIENT_ID = os.getenv("AZURE_CLIENT_ID")
AZURE_CLIENT_SECRET = os.getenv("AZURE_CLIENT_SECRET")
AZURE_TENANT_ID = os.getenv("AZURE_TENANT_ID")
AZURE_REDIRECT_URI = os.getenv("AZURE_REDIRECT_URI")
SECRET_KEY = os.getenv("SECRET_KEY")
 
if not all(
    [
        AZURE_CLIENT_ID,
        AZURE_CLIENT_SECRET,
        AZURE_TENANT_ID,
        AZURE_REDIRECT_URI,
        SECRET_KEY,
    ]
):
    raise RuntimeError(
        "Variables d'environnement manquantes. Veuillez définir "
        "AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID, "
        "AZURE_REDIRECT_URI, et SECRET_KEY."
    )
 
AUTHORITY = f"https://login.microsoftonline.com/{AZURE_TENANT_ID}"
 
# User.Read permet à l'application de demander des informations de base sur l'utilisateur Microsoft Graph.
SCOPE = ["User.Read"]
 
# -------------------------------------------------------------------
# Application FastAPI
# -------------------------------------------------------------------
 
app = FastAPI(title="Exemple FastAPI Microsoft Entra ID SSO")
 
# -------------------------------------------------------------------
# Middleware de session
# -------------------------------------------------------------------
#
# En production :
# - https_only=True
# - same_site="none"
#
# Pour le développement HTTP local, vous pouvez temporairement utiliser :
# - https_only=False
# - same_site="lax"
 
app.add_middleware(
    SessionMiddleware,
    secret_key=SECRET_KEY,
    https_only=True,
    same_site="none",
)
 
# -------------------------------------------------------------------
# Client MSAL
# -------------------------------------------------------------------
 
msal_client = msal.ConfidentialClientApplication(
    client_id=AZURE_CLIENT_ID,
    authority=AUTHORITY,
    client_credential=AZURE_CLIENT_SECRET,
)
 
# -------------------------------------------------------------------
# Routes d'authentification
# -------------------------------------------------------------------
 
async def get_microsoft_jwks():
    """Récupère les clés publiques utilisées pour signer les jetons."""
    async with httpx.AsyncClient() as client:
        response = await client.get(
            f"https://login.microsoftonline.com/{AZURE_TENANT_ID}/discovery/v2.0/keys"
        )
        return response.json()
 
 
@app.get("/login")
async def login(request: Request):
    """
    Lance le flux de connexion Microsoft Entra ID.
    """
 
    state = secrets.token_urlsafe(32)
    request.session["state"] = state
 
    authorization_url = msal_client.get_authorization_request_url(
        scopes=SCOPE,
        state=state,
        redirect_uri=AZURE_REDIRECT_URI,
    )
 
    return RedirectResponse(url=authorization_url)
 
 
@app.get("/login/callback")
async def login_callback(code: str, state: str, request: Request):
    """
    Gère le callback OAuth de Microsoft Entra ID.
    """
 
    expected_state = request.session.get("state")
 
    if not expected_state or state != expected_state:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Paramètre d'état invalide",
        )
 
    request.session.pop("state", None)
 
    token_response = msal_client.acquire_token_by_authorization_code(
        code=code,
        scopes=SCOPE,
        redirect_uri=AZURE_REDIRECT_URI,
    )
 
    if "error" in token_response:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail=token_response.get(
                "error_description",
                "Échec de l'acquisition du jeton",
            ),
        )
 
    id_token = token_response.get("id_token")
 
    if not id_token:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Aucun jeton ID retourné par Microsoft Entra ID",
        )
 
    # Récupérer les clés publiques de Microsoft pour vérifier le jeton
    jwks = await get_microsoft_jwks()
    
    # Obtenir l'en-tête non vérifié pour trouver la clé correcte
    unverified_header = jwt.get_unverified_header(id_token)
    rsa_key = {}
    for key in jwks["keys"]:
        if key["kid"] == unverified_header["kid"]:
            rsa_key = {
                "kty": key["kty"],
                "kid": key["kid"],
                "use": key["use"],
                "n": key["n"],
                "e": key["e"]
            }
            break
            
    if not rsa_key:
        raise HTTPException(status_code=401, detail="Jeton invalide : clé publique introuvable")
 
    try:
        # Nous validons l'émetteur, l'audience, l'expiration et la signature.
        id_token_claims = jwt.decode(
            id_token,
            rsa_key,
            algorithms=["RS256"],
            audience=AZURE_CLIENT_ID,
            issuer=f"https://login.microsoftonline.com/{AZURE_TENANT_ID}/v2.0"
        )
    except JWTError as e:
        raise HTTPException(status_code=401, detail=f"Échec de la validation du jeton : {str(e)}")
 
    request.session["user"] = {
        "id": id_token_claims.get("oid"),
        "name": id_token_claims.get("name"),
        "email": id_token_claims.get("preferred_username"),
        "roles": id_token_claims.get("roles", []),
    }
 
    return RedirectResponse(url="/protected")
 
 
@app.get("/logout")
async def logout(request: Request):
    """
    Efface la session locale.
    """
 
    request.session.clear()
    return RedirectResponse(url="/unprotected")
 
 
# -------------------------------------------------------------------
# Dépendances
# -------------------------------------------------------------------
 
 
def require_auth(request: Request) -> Dict[str, Any]:
    """
    Exige que l'utilisateur soit authentifié.
    """
 
    user = request.session.get("user")
 
    if not user:
        raise HTTPException(
            status_code=status.HTTP_302_FOUND,
            headers={"Location": "/login"},
        )
 
    return user
 
 
def require_roles(required_roles: List[str]):
    """
    Exige que l'utilisateur ait au moins l'un des rôles donnés.
    """
 
    def role_checker(user: Dict[str, Any] = Depends(require_auth)):
        user_roles = user.get("roles", [])
 
        if not any(role in user_roles for role in required_roles):
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Vous n'avez pas la permission d'accéder à cette ressource.",
            )
 
        return user
 
    return role_checker
 
 
# -------------------------------------------------------------------
# Routes de l'application
# -------------------------------------------------------------------
 
 
@app.get("/")
async def root():
    return {
        "message": "Exemple FastAPI Microsoft Entra ID SSO",
        "routes": {
            "login": "/login",
            "logout": "/logout",
            "unprotected": "/unprotected",
            "protected": "/protected",
            "role_protected": "/roleProtected",
        },
    }
 
 
@app.get("/unprotected")
async def unprotected_endpoint():
    return {
        "message": "Ceci est un point de terminaison non protégé.",
    }
 
 
@app.get("/protected")
async def protected_endpoint(user: Dict[str, Any] = Depends(require_auth)):
    return {
        "message": f"Bonjour, {user.get('name')}! Voici des données protégées.",
        "user_details": user,
    }
 
 
@app.get("/roleProtected")
async def role_protected_endpoint(
    user: Dict[str, Any] = Depends(require_roles(["Admin"])),
):
    return {
        "message": f"Bienvenue, Admin {user.get('name')}!",
        "detail": "Vous avez accès à ces données protégées par rôle.",
    }
 
 
if __name__ == "__main__":
    import uvicorn
 
    uvicorn.run(
        "main:app",
        host="0.0.0.0",
        port=8000,
        reload=True,
    )

Et le fichier .env correspondant :

AZURE_CLIENT_ID=votre-client-id
AZURE_CLIENT_SECRET=votre-client-secret
AZURE_TENANT_ID=votre-tenant-id
AZURE_REDIRECT_URI=https://votre-domaine.com/login/callback
SECRET_KEY=votre-secret-key

Vous pouvez trouver le code source complet ici : Lien GitHub

Si vous souhaitez déployer cette application FastAPI plus tard, vous pouvez également lire mon guide sur le déploiement d'applications sur Kubernetes.

Erreurs courantes

  • Réinitialisation des cookies : Faites attention à ce que votre navigateur ne réinitialise pas vos cookies. Assurez-vous d’utiliser les bons paramètres lors de la création des cookies, comme expliqué ci-dessus.
  • Cookies non enregistrés : Cela se produit généralement lorsque SameSite=None est utilisé sans HTTPS. Les navigateurs modernes rejettent ce cookie silencieusement.
  • Incohérence de l'URI de redirection : Assurez-vous que l'URI de redirection dans Azure correspond exactement à votre AZURE_REDIRECT_URI.
  • Test sur http, pas https : Si vous effectuez des tests sur une URL locale http://localhost, vos cookies ne seront pas enregistrés. Cela est dû au fait qu’il ne s’agit pas d’une URL HTTPS.
  • Expiration des jetons : Les jetons ont une date d’expiration. N’utilisez pas le même jeton trop longtemps lors des tests.
  • Boucle de redirection infinie après la connexion : Cela signifie souvent que le cookie de session n'est pas stocké ou que les données utilisateur ne sont pas enregistrées dans request.session.
  • Paramètre d'état invalide : Cela signifie généralement que l'state stocké avant la connexion ne correspond pas à l'state renvoyé par Microsoft.
  • Rôles manquants dans le jeton : Si roles est vide, vérifiez que les rôles d'application sont configurés dans Microsoft Entra ID et attribués à l'utilisateur ou au groupe.

Conclusion

Dans ce guide, nous avons intégré avec succès le SSO Microsoft Entra ID (Azure AD) dans une application FastAPI. Nous avons commencé par configurer l'infrastructure nécessaire dans le portail Azure, y compris l'enregistrement de l'application et les URI de redirection. Nous avons ensuite construit la logique de l'application : configuration de MSAL, gestion du flux de redirection/callback OAuth, validation de la signature JWT à l'aide des clés publiques de Microsoft et stockage de l'identité de l'utilisateur à l'intérieur d'une session de cookie signée. Enfin, nous avons appris à protéger nos points de terminaison et à implémenter le contrôle d'accès basé sur les rôles (RBAC).

En suivant ces étapes, vous disposez désormais d'une base solide pour ajouter une authentification de niveau entreprise à vos applications FastAPI. Pour les déploiements en production, n'oubliez pas d'appliquer les meilleures pratiques de sécurité abordées ci-dessus.

Lire la suite

Si vous débutez dans l'authentification web, vous aimerez peut-être aussi mon article sur le fonctionnement d'Internet. Pour le déploiement, vous pouvez suivre mon guide sur le déploiement d'applications sur Kubernetes. Pour une configuration auto-hébergée, consultez mon guide sur l'exposition d'une application Kubernetes sur Internet avec HTTPS.

#fastapi#azure#microsoft-entra-id#sso#oauth2#python
Judicael Poumay (Ph.D.)

Judicael Poumay (Ph.D.)

Suivez-moi sur LinkedIn pour du contenu hebdomadaire Judicaël Poumay

En tant que chercheur/développeur IA indépendant spécialisé en Traitement du Langage Naturel (NLP), j'ai une expertise complète dans le développement et l'intégration de systèmes d'IA, ainsi que l'analyse de données.

Votre entreprise cherche à intégrer des solutions IA, analyser des données ou renforcer son développement back-end ? Contactez-moi !

Offrez-moi une bière 🍺

Articles Similaires