AAD auth for Plotly Dash

Written by Alexander Kammerer on (last updated on )

This article shows you how to use Azure Active Directory authentication to protect your dashboards. We will implement SSO using the OAuth 2.0 flow that is supported by AAD.

The rise of data science and dashboards

The usage of dashboards to visualize and explain data to a number of internal and external clients has increased heavily in the last few years. Together with the strong buzz around data science topics, this has resulted in a lot of interesting new software, including many open source libraries. On one side, Tableau is as a large provider of ready-made solutions around data visualizations. On the other side, there is also a strong DIY community that bets on open-source technology that is sometimes assisted by paid offerings or paid consulting.

One particular interesting case is Dash by Plotly. While I myself have previously used Bokeh, I quickly made the transition to Dash since I felt it was more ready for usage as a deployed application. Additionally, having access to Plotly as a charting library is a big plus because it is such a successful open-source project with a strong community and a fantastic library.

One important thing to being able to deploy our application is of course authentication. I wanted to use SSO via AAD to secure our Dash apps. The hurdles for that are:

I want to present my usecase and then show you how I implemented it. What I wanted is:

Using flask dance

The OpenID flow is not an easy thing to implement correctly and it usually is a good idea to rely on implementations that are used by many people instead of using your own. I have found flask dance to be a great choice for this integration. It will take care of the whole OAuth 2.0 protocol flow (the “dance”) for you and all you need to do is configure the library the right way.

Let’s look at the basic setup:

from flask import Flask
from flask_dance.contrib.azure import make_azure_blueprint

blueprint = make_azure_blueprint(
    client_id="your_client_id",
    client_secret="your_client_secret",
    tenant="your_tenant_id",
    scope=["openid", "email", "profile"],
)

app = Flask(__name__)
app.register_blueprint(blueprint, url_prefix="/login")

Here, we have a flask instance and the blueprint from the flask dance library to activate the AAD authentication. What you need to do now is the following:

It is good to know what flask dance is doing in the background. And to understand that, you need to know that is necessary for the OpenID flow to work:

Putting your Dash app behind authentication

The next step is to make sure that your users can only access the Dash app if they are logged in. This means, we need to verify the login and if the user is not logged-in, prompt him to login. This can be done using decorators for the flask routes that you want to protect. Let’s take a look at the decorator that we want to use:

from flask import redirect, url_for
from flask_dance.contrib.azure import azure

def login_required(func):
    """Require a login for the given view function."""

    def check_authorization(*args, **kwargs):
        if not azure.authorized or azure.token.get("expires_in") < 0:
            return redirect(url_for("azure.login"))
        else:
            return func(*args, **kwargs)

    return check_authorization

We have done multiple things here:

Now, what is left is actually protecting the routes. For this, we assume that your flask instance is called app:

for view_func in app.view_functions:
    if not view_func.startswith("azure"):
        app.view_functions[view_func] = login_required(app.view_functions[view_func])

You need to protect all the routes of your dash server except for the login routes. To make sure to hit all the views that Dash adds without actually specifying them, we can simply cycle through all views and protect them with the login_required() decorator.

We iterate over all your view functions and protect the ones that do not start with azure. This way, we make sure that we do not protect the view functions that accept the authentication result from Microsoft and the login route that redirects the user to the Microsoft login site. This is important because the user will not be authenticated when we receive the result and only after we verified it.

Putting everything together

A full example of this would then be:

from dash import Dash, html
from werkzeug.middleware.proxy_fix import ProxyFix
from flask import Flask, redirect, url_for
from flask_dance.contrib.azure import azure, make_azure_blueprint


def login_required(func):
    """Require a login for the given view function."""

    def check_authorization(*args, **kwargs):
        if not azure.authorized or azure.token.get("expires_in") < 0:
            return redirect(url_for("azure.login"))
        else:
            return func(*args, **kwargs)

    return check_authorization

blueprint = make_azure_blueprint(
    client_id="your_client_id",
    client_secret="your_client_secret",
    tenant="your_tenant_id",
    scope=["openid", "email", "profile"],
)

app = Flask(__name__)
app.config["SECRET_KEY"] = "secretkey"
app.register_blueprint(blueprint, url_prefix="/login")

dash_app = Dash(__name__, server=app)

# use this in your production environment since you will otherwise run into problems
# https://flask-dance.readthedocs.io/en/v0.9.0/proxies.html#proxies-and-https
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)

for view_func in app.view_functions:
    if not view_func.startswith("azure"):
        app.view_functions[view_func] = login_required(app.view_functions[view_func])

dash_app.layout = html.Div(children=[
  html.H1(children='Hello Dash'),
  html.Div(children="You are logged in!")
])

if __name__ == '__main__':
    dash_app.run_server(debug=True)

Note a few things that were previously missing that I have added now:

If you have followed my instructions, you should be able to run the above code and when you visit localhost:8050, you should be forwarded to localhost:8050/login/azure that then redirects you to the login site for Microsoft. After you login, you are redirected to localhost:8050/login/azure/authorized and then finally to localhost:8050 where you should see the text You are logged in!.

Extracting the user information

One nice touch is to display the name of the user that is logged in. This is easily doable using the token that we get after the authentication flow. I am leaving it up to you how you want to integrate the token in your app, just make sure that you only use the azure variable in the context of a request as it is tied to a session that is only available when you have an actual request. This may for example interfere when you include the username directly in your Dash layout. The Dash layout is rendered before the request comes in and therefore you do not yet know who your user is. You have two options then:

Now let us take a look at the code that decods the usename from the token. I am using the pyjwt library to work with the token.

import jwt
from flask_dance.contrib.azure import azure

# no need to verify the token here, that was already done before
id_claims = jwt.decode(azure.token.get("id_token"), options={"verify_signature": False})
name = id_claims.get("name")

There is more information available in the token like the email of the user. For that, just look at the claims of the token yourself or use the website jwt.ms by Microsoft to inspect your token (your token, which essentially is a credential, never leaves your browser).

Wrap-up

Making AAD authentication work with flask is basically all you need to do to also make it work with your Dash setup. For that, we have used flask dance and configured it to work with an AAD registered app. The final touches are some details to make the OAuth flow work locally and in production and you are all set. You can then extract further details from the token like the name or the email of the user.