Click here to Skip to main content
15,503,491 members
Articles / Web Development / Flask
Article
Posted 4 Jan 2022

Stats

8.7K views
10 bookmarked

Microsoft Graph Authentication in Python

Rate me:
Please Sign up or sign in to vote.
4.79/5 (4 votes)
4 Jan 2022CPOL8 min read
In this article we create a Python web app with Flask, which made a simple Graph API call.
Here we will demonstrate how to create a Flask app that uses the Microsoft Authentication Library to get a token for accessing a Graph authentication, and then use the token to make simple calls to the Graph to verify that the token works.

This article is a sponsored article. Articles such as these are intended to provide you with information on products and services that we consider useful and of value to developers

In this article, we will be creating a Python web application with Flask, a lightweight Python web framework. We will use the Microsoft Authentication Library (MSAL) to provide us with a token and verify the user’s identity when interacting with Microsoft Graph.

App Authorization Flow

The authorization code flow for our app will be as follows:

Image 1

  1. User visits the webpage.
  2. User is redirected to authenticate through the Microsoft identity platform.
  3. User grants app permissions.
  4. User is redirected back to app with an authorization code.
  5. App uses this code to get an access token.
  6. App uses access token to call the Graph API on behalf of the user.

Set up Your Project

To complete this project, you need:

  1. Access to an environment with at least Python 2.7, or Python 3
  2. A Microsoft Azure account with an active subscription, which you can create for free
  3. A Microsoft account, which is also free to create

You can examine the complete code for this project on Github.

To keep our project contained, we are using a virtual environment with venv. This way, we can be sure that our dependencies do not interfere with anything else on our system.

Create the following:

Python
mkdir flask-ms-graph
cd flask-ms-graph
python3 -m venv venv

Next, activate the environment:

Python
venv\Scripts\activate

Our app is a simple Flask application, and so we are working with a basic project structure. If you experiment and build on this demo, you may wish to follow Flask guidelines for larger projects.

The following directory structure gives us some bare-bones architecture that we can add to. You can start by creating the following empty files:

 tree –a

├── app_config.py
├── app.py
└── templates/
   └── msal-demo/

Building a Flask app requires access to Flask, so let’s get this installed in our environment:

Python
pip install flask

Register Your App

We must register our app with Azure. This is handy to do at the beginning and is necessary to establish trust between the application and the Microsoft identity platform.

Sign in to the Azure portal and select Manage Azure Active Directory. Ensure you have selected the tenant for which you wish to create the app. You can find more information on how to set up a new tenant on Microsoft’s quickstart documentation.

In the Manage section of the sidebar, select App registrations, then New registration.

Fill in the details as in the screenshot below:

Image 2

Select Register to finish creating your app. Make a note of the Application (Client) ID, as we’ll need it later.

Image 3

Next, we generate a new client secret:

  1. Under Manage on the left bar, select Certificates & secrets.
  2. Get a new client secret, enter a description, such as "app secret," and include a duration — for example 12 months. Note the value as well, as we will need it for the configuration file we will build next.

Build the Config File

We can now fill in our config file, which contains all the settings our app will need to send to Azure AD.

Open app_config.py in your preferred editor and add the following code:

Python
CLIENT_SECRET = "Enter_the_Client_Secret_Here"
AUTHORITY = "https://login.microsoftonline.com/<enter here="" tenant_id="" the="">"
CLIENT_ID = "Enter_the_Application_Id_here"
SCOPE = ["User.ReadBasic.All"]
SESSION_TYPE = "filesystem"</enter>

Let’s examine each line for details:

CLIENT_SECRET: This should be copied from the step above. The method we’re using here is fine for a development demo but not for production. We should store our client secret in a more secure manner, perhaps using a key vault or an environment variable, as suggested by Flask. This is your regular reminder that secrets should not be committed if you are using source control.

AUTHORITY: The tenant ID identifies the Azure AD tenant to use for authentication. Our tenant ID can be found on the Overview page of your app registration in the Azure portal.

CLIENT_ID: This is our application ID, which we just created on Azure AD. You can copy this value from above, and it will be in the standard (8-4-4-4-12) UUID format.

SCOPE: This is the permission level that the app is requesting. Azure AD will check that the app has consent, which is presented to the user on sign in. If consent is granted, Microsoft identity platform will provide an access token encoded with these scopes. We want to grant the app privileges to see the user’s resources. For more details on this process, see the Microsoft Graph permissions reference. In this case, User.ReadBasic.All will grant the app permission to read the user’s basic profile, which will be necessary for our Graph call. As always, for security reasons, we should only provide the minimum necessary privileges.

SESSION_TYPE: Setting this to filesystem will ensure the session information is stored in the file system where we will run our app — probably your PC. In our case, Flask will manage our session for us locally. In production, this can be set to a database like MongoDB or SQLalchemy. When running our app, a session folder is created containing a token.

Create a Session

We are using the Flask extension Flask-Session to store our sessions server-side. Flask does have sessions, but they are client-side — the data about the session is stored in a cookie and sent back to the server with every request.

By using Flask-Session, we avoid sending data to the client. This is far more secure; even though we still use cookies, they contain a session ID rather than any sensitive information.

Let’s install Flask-Session with the following code:

$ pip install Flask-Session

In app.py, enter the following code:

Python
import uuid
import requests
from flask import Flask, render_template, session, request, redirect, url_for
from flask_session import Session
import msal

import app_config


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


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

For tidiness, we have brought in all our imports at the beginning. We will begin to use these as we build our file.

We now have the bare bones of our app. It loads our config and starts our session. Flask-Session integrates with Flask’s session object, so we will continue using this as normal. Since SESSION_TYPE has been set to filesystem, our session will be stored locally.

Create the Homepage

We would like our app to render a homepage if users are logged in, or redirect them to a login page if they are not, so let’s add some code to check login status and redirect as needed.

In app.py, after Session(app), add the following code:

Python
@app.route("/")
def index():
    if not session.get("user"):
        return redirect(url_for("login"))
    return render_template('index.html', user=session["user"])

Let’s now create the default index.html.

In the templates directory, create an index.html file with the following contents:

HTML
{% extends "base.html" %}
{% block mainheader %}Welcome{% endblock %}
{% block content %}
  <a class="btn btn-primary btn-lg" href="/msal-demo" role="button">MSAL Demo</a>
  <a class="btn btn-danger btn-lg" href="/logout" role="button">Logout</a>
</body>
{% endblock %}

This extends base.html, which we must now create in the templates directory. It displays two buttons: one for the Graph API call, and one to log out.

In templates/base.html, enter the following:

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

<head>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
        integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"
        integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"
        crossorigin="anonymous"></script>
    <title>Python Graph Demo - {% block title %}{% endblock %}</title>
</head>

<body>
    <div class="container mt-4">
        <div class="jumbotron">
            <h1 class="display-4">Python Flask App - {% block mainheader %}{% endblock %}</h1>
            <hr class="my-4">

            <div id="content">{% block content %}{% endblock %}</div>
        </div>
    </div>
</body>

</html>

We use Bootstrap style sheets to give our UI a modern look with little effort.

Create the Token Cache

Access tokens are acquired on behalf of the app, not the user. They enable the app to securely call web APIs that are protected by Azure AD. These tokens are typically Base64-encoded JWT.

To have a persistent token cache in our MSAL Python app, we must provide custom token cache serialization. Let’s get MSAL in our environment:

pip install msal

Enter the following code into your app.py file:

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

This function, _get_token_from_cache, will be used for our API calls to ensure we have the correct token with the correct scope permissions for our app.

We also must create a function in the app.py file, get_token, to get the token from the session cache and redirect to the login page if it isn’t found. We will use this in every function that needs to make a call on behalf of the user, so we can minimize duplicate code.

Python
def get_token(scope):
    token = _get_token_from_cache(scope)
    if not token:
        return redirect(url_for("login"))
    return token

Create the Login/Logout

As developers, configuring the security of web applications is often met with uneasiness. Let’s hand this stress over to Microsoft, who can sort out the authentication process for us.

Here, we acquire our token and cache it. Add the following code to app.py:

Python
@app.route("/login")
def login():
    session["state"] = str(uuid.uuid4())
    auth_url = _build_msal_app().get_authorization_request_url(
        app_config.SCOPE,
        state=session["state"],
        redirect_uri=url_for("authorized", _external=True))
    return "<a href='%s'>Login with Microsoft Identity</a>" % auth_url

@app.route("/getAToken")  # This absolute URL must match your app's redirect_uri set in AAD
def authorized():
    if request.args['state'] != session.get("state"):
        return redirect(url_for("login"))
    cache = _load_cache()
    result = _build_msal_app(cache).acquire_token_by_authorization_code(
        request.args['code'],
        scopes=app_config.SCOPE,
        redirect_uri=url_for("authorized", _external=True))
    if "error" in result:
        return "Login failure: %s, %s" % (
            result["error"], result.get("error_description"))
    session["user"] = result.get("id_token_claims")
    _save_cache(cache)
    return redirect(url_for("index"))

With this, we’ve handed over authentication of our user to Microsoft. We can make use of features like forgotten password recovery and account creation without having to worry about creating bespoke pages, cutting down on development time and future code management.

Let’s now give our user an option to log out. We can clear the session and log out of the Microsoft Identity Platform:

Python
@app.route("/logout")
def logout():
    session.clear()  # Wipe out the user and the token cache from the session
    return redirect(  # Also need to log out from the Microsoft Identity platform
        "https://login.microsoftonline.com/common/oauth2/v2.0/logout"
        "?post_logout_redirect_uri=" + url_for("index", _external=True))

Add the Graph Call

In this series of articles, we are building a Web App which will access the Graph API, so to prove everything works as expected, let’s make a simple Graph API call.

Add the following code to app.py:

Python
@app.route("/msal-demo")
def msal_demo():
    if not session.get("user"):
        return redirect(url_for("login"))
    token = get_token(app_config.SCOPE)
    graph_data = requests.get(  # Use token to call downstream service
        'https://graph.microsoft.com/v1.0/me',
        headers={'Authorization': 'Bearer ' + token['access_token']},
    ).json()        
    return render_template('msal-demo/index.html', result=graph_data, user=session["user"])

We also need to add an index.html file to our templates/msal-demo directory. Create this file and add the following code:

HTML
{% extends "base.html" %}
{% block mainheader %}Microsoft Identity{% endblock %}
{% block content %}
  <a class="btn btn-primary btn-md" href="/" role="button">Back Home</a>
  <a class="btn btn-danger btn-md" href="/logout" role="button">Logout</a>
  <pre>{{ result |tojson(indent=4) }}</pre>
{% endblock %}

This code extends base.html in the templates directory, which we have already added. Here, we have also added a back button and a logout button.

Final Setup

Before we test our app, we need to update Azure AD to permit the redirect URIs. This is a great security feature because from the login, our app can only be redirected back to URIs that we have configured on Azure AD. For more information on the formats allowed, consult Microsoft’s documentation on URIs. We should remain mindful of security by ensuring redirect URIs are absolute and wildcard globs are avoided when possible.

App in Action

To test our app, run:

flask run --port=5000 --host=localhost

Image 4

Image 5

We have now created a fully functional Python web app that allows a user to log in and make a basic Graph call that returns their user profile information and renders the result. As we'll see over the course of the following two articles, this will provide a great base application to build upon. In the next article we will extend this app by adding functionality to enable our users to view a list of their OneNote pages, select a page, view its HTML, and download the page as Markdown.

This article is part of the series 'Building on the Microsoft Graph with Python View All

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA6-Jan-22 0:16
professionalȘtefan-Mihai MOGA6-Jan-22 0:16 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.