AAD auth for Plotly Dash
Tags: azure, oauth, aad, dash
Published: 2022-04-03 18:28:51

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](https://plotly.com/dash/). While I myself have previously used [Bokeh](https://bokeh.org/), 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:

- There is both a huge number of articles detailing AAD integration and not very many useful articles. There are so many usecases 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 usecase that may not fit yours.
- Authentication with AAD uses the OpenID flow of the OAuth protocol. The OAuth procotol is quite difficult to understand already and it is not helped by the fact that every provider implements it a little different.

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

- Implement SSO using AAD so that a user can authenticate to the Dash app using his AAD account.
- I want to read the user's name and email from the issued OpenID token so that I can use this in my app.

## 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](https://flask-dance.readthedocs.io/) 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:

```python
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:

- 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. Microsoft has a great [tutorial](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) that will guide you through the process. Follow the part of the tutorial that is called [Register an application](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app#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](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app#add-credentials) section.
- Replace the placeholders in the `make_azure_blueprint()` call with your values.
- Add `localhost:8050/login/azure/authorized` [to your redirect URIs](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app#add-a-redirect-uri) in the app registration.

It is good to know what [flask dance is doing in the background](https://flask-dance.readthedocs.io/en/latest/understanding-the-magic.html). And to understand that, you need to know that 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 he inputs his credentials and authenticates with his Microsoft account. Flask dance adds a route to your flask app that redirects you to the Microsoft endpoint for authentication.
- Then, if the user logged in on the Microsoft page, you will eventually get different tokens (in the the form of [JWTs](https://jwt.io/introduction)) 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 login, 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 with the help of the response the tokens are retrieved and verified.
- Optional: if you want, you can use one of these tokens to authenticate as the user to Microsoft APIs, if the user has given you access. If you only want SSO, make sure your scope is only `scope=["openid", "email", "profile"]` and nothing else.

## 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:

```python
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. For this, we assume that your flask instance is called `app`:

```python
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:

```python
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:

- Make sure to use a long string for the `SECRET_KEY` and consult the [flask documentation](https://flask.palletsprojects.com/en/2.1.x/config/#SECRET_KEY). 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) because otherwise the authorization will not work. This is due to the redirect URIs that Microsoft allows.
- Make sure to apply the [proxy fix](https://flask-dance.readthedocs.io/en/v0.9.0/proxies.html#proxies-and-https) if you are using this in production behind an SSL proxy.
- In production, you will need to add the URL `https://yourdomain.com/login/azure/authorized` to your redirect URIs for your AD registered app. Of course, make sure to use the actual URL of your app instead of `https://yourdomain.com`.
- 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 as we requested it.
- **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 login token will not be transported via SSL.

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:

- 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 live in request contexts and therefore you can use the `azure` variable there.

Now let us take a look at the code that decods the usename from the token. I am using the [`pyjwt`](https://pyjwt.readthedocs.io/en/stable/) library to work with the token.

```python
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](https://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.