AAD authentication for Plotly Dash
This article shows you how to use Azure Active Directory (AAD) 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 turned into a business by paid support or consulting.
One particular interesting case is?Dash by Plotly. Some developers at finccam have previously used?Bokeh, but quickly made the transition to Dash since they 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. At finccam, we use dashboards made with Dash everyday.
One important part of the deployment of any application is of course authentication. We wanted to use single-sign-on (SSO) via AAD to secure our Dash apps. The hurdles for that are:
We want to present our use-case in detail and then show you how we implemented it. What we wanted is:
Using flask dance
The OpenID flow is not an easy thing to implement correctly and it usually is good advice to rely on implementations that are used by many people instead of using your own. We 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") and all you need to do is configure the library.
Let's look at the basic setup which is actually quite short:
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 create a flask instance and use the blueprint from the flask dance library to activate the AAD authentication. Additionally, what you need to do now is the following:
It is good to know some of the things that?flask dance is doing in the background. And to understand that, you need to know what 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 if the user is logged-in and if not, 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 (or views). 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 (if the user is not logged-in, you want to be able to log him in). 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 this result.
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 we have added now:
If you have followed these 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. This site then redirects you to the login site by 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. We are 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 by default rendered at server startup, i.e. before any request or session. You have two options then:
Now let us take a look at the code that decodes the username from the token. I am using the?pyjwt?library to work with the token (here, the verification of the token is already done for us, but in general it actually is very important to use a library that supports the JWT standard and is kept up to date).
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 - do not put your token into any input on any website you do not trust).
Wrap-up
Making AAD authentication work with flask is basically all you need to do to also make it work with your Dash app. 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. You can then extract further details from the token like the name or the email of the user.
P.S. We are consistently looking for passionate people to join us on our journey. Our current career opportunities can be found on our?career page.
Head of AI EMEA for FSI &Telco @Oracle | CTO | Ex-traordinaire
2 年Maybe you have looked at it already, but I would highly recommend streamlit not as a replacement, but as one more tool in your dashboarding/lightweight interactive apps toolset.