Verificatie inschakelen in uw eigen Python-webtoepassing met behulp van Azure Active Directory B2C

In dit artikel leert u hoe u Azure AD B2C-verificatie (Azure Active Directory B2C) toevoegt in uw eigen Python-webtoepassing. U kunt gebruikers in staat stellen om zich aan of af te melden, hun profiel bij te werken, en het wachtwoord opnieuw in te stellen met behulp van Azure AD B2C-gebruikersstromen. In dit artikel wordt MSAL (Microsoft Authentication Library) voor Python gebruikt om het toevoegen van verificatie aan uw Python-webtoepassing te vereenvoudigen.

Het doel van dit artikel is om de voorbeeldtoepassing die u hebt gebruikt in Verificatie configureren in een Python-voorbeeldwebtoepassing door gebruik te maken van Azure AD B2C, te vervangen door uw eigen Python-toepassing.

In dit artikel wordt Python 3.9+ en Flask 2.1 gebruikt om een eenvoudige web-app te maken. De weergaven van de toepassing maken gebruik van Jinja2-sjablonen.

Vereisten

Stap 1: Het Python-project maken

  1. Maak in uw bestandssysteem een projectmap voor deze zelfstudie, zoals my-python-web-app.

  2. Wijzig in de terminal de map van uw Python-app, zoals cd my-python-web-app.

  3. Voer de volgende opdracht uit om een virtuele omgeving met de naam .venv te maken en te activeren op basis van uw huidige interpreter.

    sudo apt-get install python3-venv  # If needed
    python3 -m venv .venv
    source .venv/bin/activate
    
  4. Werk pip bij in de virtuele omgeving door de volgende opdracht uit te voeren in de terminal:

    python -m pip install --upgrade pip
    
  5. Als u de Flask-foutopsporingsfuncties wilt inschakelen, schakelt u Flask over naar de ontwikkelomgeving voor de modus development. Raadpleeg de Flask-documentatie voor meer informatie over het opsporen van fouten in Flask-apps.

    export FLASK_ENV=development
    
  6. Open de projectmap in VS Code door de opdracht code .uit te voeren, of door VS Code te openen en Bestand>Map openen te selecteren.

Stap 2: App-afhankelijkheden installeren

Maak het bestand requirements.txt onder de hoofdmap van uw web-app. Het vereistenbestand bevat de pakketten die moeten worden geïnstalleerd met behulp van pip-installatie. Voeg de volgende inhoud toe aan het bestand vereisten.txt:

Flask>=2
werkzeug>=2

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

Installeer in de terminal de afhankelijkheden door de volgende opdrachten uit te voeren:

python -m pip install -r requirements.txt

Stap 3: Onderdelen van de app-gebruikersinterface bouwen

Flask is een lichtgewicht Python-framework voor webtoepassingen, dat de basisbeginselen biedt voor URL-routering en paginaweergave. Het gebruikt Jinja2 als sjabloonengine om de inhoud van apps weer te geven. Raadpleeg de documentatie voor sjabloonontwerpers voor meer informatie. In deze sectie voegt u de vereiste sjablonen toe die de basisfunctionaliteit van uw web-app bieden.

Stap 3.1: Een basissjabloon maken

Een basispaginasjabloon in Flask bevat alle gedeelde onderdelen van een set pagina's, inclusief verwijzingen naar CSS-bestanden, scriptbestanden, enzovoort. Basissjablonen definiëren ook een of meer bloktags die andere sjablonen die de basis uitbreiden, naar verwachting moeten overschrijven. Een bloktag wordt uitgelijnd door {% block <name> %} en {% endblock %}, in zowel de basissjabloon als de uitgebreide sjabloon.

Maak de map templates in de hoofdmap van uw web-app. Maak in de map Sjablonen een bestand met de naam base.html. Voeg vervolgens de onderstaande inhoud toe:

<!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>

Stap 3.2: De web-app-sjablonen maken

Voeg de volgende sjablonen toe onder de map Sjablonen. Met deze sjablonen wordt de sjabloon base.html uitgebreid:

  • index.html: de startpagina van de web-app. De sjablonen gebruiken de volgende logica: als een gebruiker zich niet aanmeldt, wordt de aanmeldingsknop weergegeven. Als een gebruiker zich aanmeldt, worden de claims van het toegangstoken weergegeven, een koppeling naar het profiel bewerken en een Graph API aangeroepen.

    {% 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: laat zien hoe u een REST API aanroept.

    {% 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: verwerkt verificatiefouten.

    {% 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 %}
    

Stap 4: Uw web-app configureren

Maak een bestand met de naam app_config.py in de hoofdmap van uw web-app. Dit bestand bevat informatie over uw Azure AD B2C-id-provider. De web-app gebruikt deze informatie om een vertrouwensrelatie tot stand te brengen met Azure AD B2C, gebruikers aan te melden en af te melden, tokens te verkrijgen en deze te valideren. Voeg de volgende inhoud toe aan het bestand:

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

Werk de bovenstaande code bij met uw Azure AD B2C-omgevingsinstellingen, zoals wordt uitgelegd in de sectie De voorbeeldweb-app configureren van het artikel Verificatie configureren in een Python-voorbeeldweb-app.

Stap 5: De code van de web-app toevoegen

In deze sectie voegt u de Flask-weergavefuncties en de verificatiemethoden van de MSAL-bibliotheek toe. Voeg onder de hoofdmap van uw project een bestand met de naam app.py toe, met de volgende code:

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()

Stap 6: Uw web-app uitvoeren

Voer de app uit in de terminal door de volgende opdracht in te voeren, waarmee de Flask-ontwikkelserver wordt uitgevoerd. De ontwikkelserver zoekt standaard naar app.py. Open vervolgens de browser, en navigeer naar de URL van de web-app: http://localhost:5000.

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

[Optioneel] Fouten opsporen in uw app

De functie voor foutopsporing biedt u de mogelijkheid om een actief programma op een bepaalde regel code te onderbreken. Wanneer u het programma onderbreekt, kunt u variabelen onderzoeken, code uitvoeren in de Foutopsporingsconsole en anderszins profiteren van de functies die worden beschreven in Foutopsporing. Als u het foutopsporingsprogramma van Visual Studio Code wilt gebruiken, raadpleegt u de VS Code-documentatie.

Als u de hostnaam en/of het poortnummer wilt wijzigen, gebruikt u de args-matrix van het bestand launch.json. In het volgende voorbeeld ziet u hoe u de hostnaam configureert naar localhost en het poortnummer naar 5001. Als u de hostnaam of het poortnummer wijzigt, moet u de omleidings-URI of uw toepassing bijwerken. Raadpleeg de stap Een webtoepassing registreren voor meer informatie.

{
    // 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
        }
    ]
}

Volgende stappen