AAD authentication for Plotly Dash

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:

  • There is both a huge number of articles detailing AAD integration and not very many useful articles. There are so many use-cases for authentication (and authorization) with AAD that it is hard to find exactly what you are looking for.
  • There are multiple packages that claim to support AAD authentication for flask. But some of them are very heavily built around one use-case that may not fit yours (like single page applications).
  • Authentication with AAD uses the OpenID flow of the OAuth 2.0 protocol. The OAuth protocol is quite difficult to understand already and it is not helped by the fact that every provider implements it a little different within the boundaries of the specification.

We want to present our use-case in detail and then show you how we implemented it. What we wanted is:

  • Implement SSO using AAD so that a user can authenticate to the Dash app using his AAD account.
  • We want to read the user's name and email from the issued OpenID token so that we can use this in my app.
  • We are not using the access token to authenticate the user to any Microsoft API (although we could with our setup).

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:

  • You need to create an app registration in your AAD blade in the Azure portal. This will give you a client ID and a tenant ID (used above). Microsoft has a great?tutorial?that will guide you through the process. Follow the part of the tutorial that is called?Register an application.
  • You need to create a client secret for usage in your deployment or local development. This is described in the?Add credentials?section of the previously mentioned tutorial. You should treat this secret with the care that you would treat any secret with.
  • Replace the placeholders in the?make_azure_blueprint()?call with your tenant and client ID and your client secret.
  • Add?localhost:8050/login/azure/authorized?to your redirect URIs?in the app registration. (You will need to add another URL once you have deployed your application. Make sure to read our advice for deployments below.)

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:

  • The first step is to send a request for authentication to Microsoft. For that, you redirect the user to some login site by Microsoft where they input their credentials and authenticate with their AAD account. Flask dance will add a route to your flask app that redirects to the this site and configure the necessary parameters in the URL.
  • Then, if the user logged in successfully on the Microsoft page, you will eventually get different tokens (in the the form of?JWTs) that serve the purpose to verify the authentication (open id token), allow access to Microsoft services (access token), and renew the access token (refresh token). If you are only interested in the identity of the user, only the open id token is of interest to you.
  • Flask dance will add a route to your flask server so that the response from Microsoft can be parsed and the tokens verified.
  • Optional: if you want, you can use the access token to authenticate as the user to Microsoft APIs, if the user has given you access to these APIs. If you are only interested in the user's identity, set the scope to scope=["openid", "email", "profile"].

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:

  • We wrap the given function so that we first check if the user is authorized.
  • If the user is authorized, we call the original function and return the result.
  • If the user is not authorized or the token is expired, we redirect the user to login again.

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:

  • Make sure to use a long and safe string for the?SECRET_KEY?and consult the?flask documentation. You will need to set it because flask dance uses the session to store the tokens for the user.
  • You can only access the app via localhost (not 127.0.0.1 or anything else) because otherwise the OpenID flow will not work. This is due to the restrictions Microsoft puts on redirect URIs (see above).
  • Make sure to apply the?proxy fix?if you are using this in production behind an SSL proxy. Otherwise, your app will not work.
  • In production, you will need to add the URL?https://replacewithyourdomain.com/login/azure/authorized?to your redirect URIs for your AD registered app.
  • To make the authentication work, you need to add?OAUTHLIB_RELAX_TOKEN_SCOPE=1?to your environment variables configuration (for development and production). This is because AAD does not guarantee to return the scope in the same order or even the same scope that we requested.
  • Only and only for local development?will you need to add?OAUTHLIB_INSECURE_TRANSPORT=1?to your environment variables to make the authentication work. This is because locally, the tokens will not be transported via SSL.

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:

  • Make the Dash layout a function that is then rendered each time a user visits the page (and therefore you have a request context).
  • Make the username the output of a callback. Callbacks always have a request context and therefore you can use the?azure?variable there.

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.

Abdelali Zahi

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.

要查看或添加评论,请登录

finccam的更多文章

社区洞察

其他会员也浏览了