Activer l’authentification dans votre propre application web Python à l’aide d’Azure Active Directory B2C

Dans cet article, vous allez apprendre à ajouter l’authentification Azure Active Directory B2C (Azure AD B2C) dans votre propre application web Python. Vous permettez aux utilisateurs de se connecter, de se déconnecter, de mettre à jour leur profil et de réinitialiser leur mot de passe à l’aide des flux d’utilisateurs Azure AD B2C. Cet article utilise la bibliothèque d’authentification Microsoft (MSAL) pour Python afin de simplifier l’ajout de l’authentification à votre application web Python.

L’objectif de cet article est de remplacer l’exemple d’application que vous avez utilisé dans Configurer l’authentification dans un exemple d’application web Python à l’aide d’Azure AD B2C par votre propre application web Python.

Cet article utilise Python 3.9+ et Flask 2.1 pour créer une application web de base. Les vues de l’application utilisent des modèles Jinja2.

Prérequis

Étape 1 : Créer le projet Python

  1. Sur votre système de fichiers, créez un dossier de projet pour ce didacticiel, par exemple my-python-web-app.

  2. Dans votre terminal, accédez au répertoire de votre application Python, par exemple cd my-python-web-app.

  3. Exécutez la commande suivante pour créer et activer un environnement virtuel nommé .venv en fonction de votre interpréteur actuel.

    sudo apt-get install python3-venv  # If needed
    python3 -m venv .venv
    source .venv/bin/activate
    
  4. Mettez à jour pip dans l’environnement virtuel en exécutant la commande suivante dans le terminal :

    python -m pip install --upgrade pip
    
  5. Pour activer les fonctionnalités de débogage Flask, basculez Flask vers l’environnement de développement en mode development. Pour plus d’informations sur le débogage d’applications Flask, consultez la documentation Flask.

    export FLASK_ENV=development
    
  6. Ouvrez le dossier de projet dans VS Code en exécutant la commande code ., ou en ouvrant VS Code et en sélectionnant le Fichier>Ouvrir le dossier.

Étape 2 : installer les dépendances d’application

Sous votre dossier racine de l’application web, créez le fichier requirements.txt. Le fichier requis répertorie les packages à installer à l’aide de pip install. Ajoutez le contenu suivant au fichier requirements.txt :

Flask>=2
werkzeug>=2

flask-session>=0.3.2,<0.5
requests>=2,<3
msal>=1.7,<2

Dans votre terminal, installez les dépendances en exécutant les commandes suivantes :

python -m pip install -r requirements.txt

Étape 3 : générer les composants de l’interface utilisateur de l’application

Flask est un framework Python léger pour les applications web, qui fournit les éléments de base pour le routage d’URL et le rendu de page. Il tire parti de Jinja2 comme moteur de modèle pour afficher le contenu de votre application. Pour plus d’informations, consultez la documentation du concepteur de modèles. Dans cette section, vous ajoutez les modèles requis qui fournissent les fonctionnalités de base de votre application web.

Étape 3.1 Créer un modèle de base

Un modèle de page de base dans Flask contient toutes les parties partagées d’un ensemble de pages, y compris les références aux fichiers CSS, aux fichiers de script, etc. Les modèles de base définissent également une ou plusieurs balises de bloc que d’autres modèles qui étendent la base sont censés remplacer. Une balise de bloc est délimitée par {% block <name> %} et {% endblock %} dans le modèle de base et dans le modèle étendu.

Dans le dossier racine de votre application web, créez le dossier templates. Dans le dossier modèles, créez un fichier nommé base.html, puis ajoutez le contenu ci-dessous :

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    {% block metadata %}{% endblock %}

    <title>{% block title %}{% endblock %}</title>
    <!-- Bootstrap CSS file reference -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor" crossorigin="anonymous">
</head>

<body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <div class="container-fluid">
            <a class="navbar-brand" href="{{ url_for('index')}}">Python Flask demo</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse"
                data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
                aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                    <li class="nav-item">
                        <a class="nav-link active" aria-current="page" href="{{ url_for('index')}}">Home</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="{{ url_for('graphcall')}}">Graph API</a>
                    </li>
                </ul>
            </div>
        </div>
    </nav>

    <div class="container body-content">
        <br />
        {% block content %}
        {% endblock %}

        <hr />
        <footer>
            <p>Powered by MSAL Python {{ version }}</p>
        </footer>
    </div>
</body>

</html>

Étape 3.2 Créer les modèles d’application web

Ajoutez les modèles suivants sous le dossier modèles. Ces modèles étendent le modèle base.html :

  • index.html : page d’accueil de l’application web. Les modèles utilisent la logique suivante : si un utilisateur ne se connecte pas, il affiche le bouton de connexion. Si un utilisateur se connecte, il affiche les revendications du jeton d’accès, un lien pour modifier le profil et appeler un API Graph.

    {% extends "base.html" %}
    {% block title %}Home{% endblock %}
    {% block content %}
    
    <h1>Microsoft Identity Python Web App</h1>
    
    {% if user %}
    <h2>Claims:</h2>
    <pre>{{ user |tojson(indent=4) }}</pre>
    
    
    {% if config.get("ENDPOINT") %}
    <li><a href='/graphcall'>Call Microsoft Graph API</a></li>
    {% endif %}
    
    {% if config.get("B2C_PROFILE_AUTHORITY") %}
    <li><a href='{{_build_auth_code_flow(authority=config["B2C_PROFILE_AUTHORITY"])["auth_uri"]}}'>Edit Profile</a></li>
    {% endif %}
    
    <li><a href="/logout">Logout</a></li>
    
    {% else %}
    <li><a href='{{ auth_url }}'>Sign In</a></li>
    {% endif %}
    
    {% endblock %}
    
  • graph.html : montre comment appeler une API REST.

    {% extends "base.html" %}
    {% block title %}Graph API{% endblock %}
    {% block content %}
    <a href="javascript:window.history.go(-1)">Back</a>
    <!-- Displayed on top of a potentially large JSON response, so it will remain visible -->
    <h1>Graph API Call Result</h1>
    <pre>{{ result |tojson(indent=4) }}</pre> <!-- Just a generic json viewer -->
    {% endblock %}
    
  • auth_error.html : gère les erreurs d’authentification.

    {% extends "base.html" %}
    {% block title%}Error{% endblock%}
    
    {% block metadata %}
    {% if config.get("B2C_RESET_PASSWORD_AUTHORITY") and "AADB2C90118" in result.get("error_description") %}
    <!-- See also https://learn.microsoft.com/azure/active-directory-b2c/active-directory-b2c-reference-policies#linking-user-flows -->
    <meta http-equiv="refresh"
      content='0;{{_build_auth_code_flow(authority=config["B2C_RESET_PASSWORD_AUTHORITY"])["auth_uri"]}}'>
    {% endif %}
    {% endblock %}
    
    {% block content %}
    <h2>Login Failure</h2>
    <dl>
      <dt>{{ result.get("error") }}</dt>
      <dd>{{ result.get("error_description") }}</dd>
    </dl>
    
    <a href="{{ url_for('index') }}">Homepage</a>
    {% endblock %}
    

Étape 4 : configurer votre application web

Dans le dossier racine de votre application web, créez un fichier nommé app_config.py. Ce fichier contient des informations sur votre fournisseur d’identité Azure AD B2C. L’application web utilise ces informations pour établir une relation de confiance avec Azure AD B2C, connecter et déconnecter les utilisateurs et acquérir des jetons et les valider. Ajoutez le contenu suivant au fichier :

import os

b2c_tenant = "fabrikamb2c"
signupsignin_user_flow = "B2C_1_signupsignin1"
editprofile_user_flow = "B2C_1_profileediting1"

resetpassword_user_flow = "B2C_1_passwordreset1"  # Note: Legacy setting.

authority_template = "https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/{user_flow}"

CLIENT_ID = "Enter_the_Application_Id_here" # Application (client) ID of app registration

CLIENT_SECRET = "Enter_the_Client_Secret_Here" # Application secret.

AUTHORITY = authority_template.format(
    tenant=b2c_tenant, user_flow=signupsignin_user_flow)
B2C_PROFILE_AUTHORITY = authority_template.format(
    tenant=b2c_tenant, user_flow=editprofile_user_flow)

B2C_RESET_PASSWORD_AUTHORITY = authority_template.format(
    tenant=b2c_tenant, user_flow=resetpassword_user_flow)

REDIRECT_PATH = "/getAToken"

# This is the API resource endpoint
ENDPOINT = '' # Application ID URI of app registration in Azure portal

# These are the scopes you've exposed in the web API app registration in the Azure portal
SCOPE = []  # Example with two exposed scopes: ["demo.read", "demo.write"]

SESSION_TYPE = "filesystem"  # Specifies the token cache should be stored in server-side session

Mettez à jour le code ci-dessus avec vos paramètres d’environnement Azure AD B2C, comme expliqué dans la section Configurer l’exemple d’application web de l’article Configurer l’authentification dans un exemple d’application web Python.

Étape 5 : Ajouter le code de l’application web

Dans cette section, vous ajoutez les fonctions d’affichage Flask et les méthodes d’authentification de bibliothèque MSAL. Sous le dossier racine de votre projet, ajoutez un fichier nommé app.py avec le code suivant :

import uuid
import requests
from flask import Flask, render_template, session, request, redirect, url_for
from flask_session import Session  # https://pythonhosted.org/Flask-Session
import msal
import app_config


app = Flask(__name__)
app.config.from_object(app_config)
Session(app)

# This section is needed for url_for("foo", _external=True) to automatically
# generate http scheme when this sample is running on localhost,
# and to generate https scheme when it is deployed behind reversed proxy.
# See also https://flask.palletsprojects.com/en/1.0.x/deploying/wsgi-standalone/#proxy-setups
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)


@app.route("/anonymous")
def anonymous():
    return "anonymous page"

@app.route("/")
def index():
    #if not session.get("user"):
    #    return redirect(url_for("login"))

    if not session.get("user"):
        session["flow"] = _build_auth_code_flow(scopes=app_config.SCOPE)
        return render_template('index.html', auth_url=session["flow"]["auth_uri"], version=msal.__version__)
    else:
        return render_template('index.html', user=session["user"], version=msal.__version__)

@app.route("/login")
def login():
    # Technically we could use empty list [] as scopes to do just sign in,
    # here we choose to also collect end user consent upfront
    session["flow"] = _build_auth_code_flow(scopes=app_config.SCOPE)
    return render_template("login.html", auth_url=session["flow"]["auth_uri"], version=msal.__version__)

@app.route(app_config.REDIRECT_PATH)  # Its absolute URL must match your app's redirect_uri set in AAD
def authorized():
    try:
        cache = _load_cache()
        result = _build_msal_app(cache=cache).acquire_token_by_auth_code_flow(
            session.get("flow", {}), request.args)
        if "error" in result:
            return render_template("auth_error.html", result=result)
        session["user"] = result.get("id_token_claims")
        _save_cache(cache)
    except ValueError:  # Usually caused by CSRF
        pass  # Simply ignore them
    return redirect(url_for("index"))

@app.route("/logout")
def logout():
    session.clear()  # Wipe out user and its token cache from session
    return redirect(  # Also logout from your tenant's web session
        app_config.AUTHORITY + "/oauth2/v2.0/logout" +
        "?post_logout_redirect_uri=" + url_for("index", _external=True))

@app.route("/graphcall")
def graphcall():
    token = _get_token_from_cache(app_config.SCOPE)
    if not token:
        return redirect(url_for("login"))
    graph_data = requests.get(  # Use token to call downstream service
        app_config.ENDPOINT,
        headers={'Authorization': 'Bearer ' + token['access_token']},
        ).json()
    return render_template('graph.html', result=graph_data)


def _load_cache():
    cache = msal.SerializableTokenCache()
    if session.get("token_cache"):
        cache.deserialize(session["token_cache"])
    return cache

def _save_cache(cache):
    if cache.has_state_changed:
        session["token_cache"] = cache.serialize()

def _build_msal_app(cache=None, authority=None):
    return msal.ConfidentialClientApplication(
        app_config.CLIENT_ID, authority=authority or app_config.AUTHORITY,
        client_credential=app_config.CLIENT_SECRET, token_cache=cache)

def _build_auth_code_flow(authority=None, scopes=None):
    return _build_msal_app(authority=authority).initiate_auth_code_flow(
        scopes or [],
        redirect_uri=url_for("authorized", _external=True))

def _get_token_from_cache(scope=None):
    cache = _load_cache()  # This web app maintains one cache per session
    cca = _build_msal_app(cache=cache)
    accounts = cca.get_accounts()
    if accounts:  # So all account(s) belong to the current signed-in user
        result = cca.acquire_token_silent(scope, account=accounts[0])
        _save_cache(cache)
        return result

app.jinja_env.globals.update(_build_auth_code_flow=_build_auth_code_flow)  # Used in template

if __name__ == "__main__":
    app.run()

Étape 6 : exécuter votre application web

Dans le terminal, exécutez l’application en entrant la commande suivante, qui exécute le serveur de développement Flask. Ce dernier recherche le fichier app.py par défaut. Ensuite, ouvrez votre navigateur et accédez à l’URL de l’application web : http://localhost:5000.

python -m flask run --host localhost --port 5000

[Facultatif] Déboguer votre application

La fonctionnalité de débogage vous permet de suspendre un programme en cours d’exécution sur une ligne de code particulière. Lorsque vous interrompez le programme, vous pouvez examiner des variables, exécuter du code dans le panneau Console de débogage, et tirer parti des fonctionnalités décrites sur le débogage. Pour utiliser le débogueur Visual Studio Code, consultez la documentation VS Code.

Pour modifier le nom d’hôte et/ou le numéro de port, utilisez le tableau args du fichier launch.json. L’exemple suivant montre comment configurer le nom d’hôte sur localhost et le numéro de port sur 5001. Notez que si vous modifiez le nom d’hôte ou le numéro de port, vous devez mettre à jour l’URI de redirection ou votre application. Pour plus d’informations, consultez l’étape Inscrire une application web.

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Flask",
            "type": "python",
            "request": "launch",
            "module": "flask",
            "env": {
                "FLASK_APP": "app.py",
                "FLASK_ENV": "development"
            },
            "args": [
                "run",
                "--host=localhost",
                "--port=5001"
            ],
            "jinja": true,
            "justMyCode": true
        }
    ]
}

Étapes suivantes