Dash App Pages with Flask Login Flow using Flask

It’s happening something weird, the app does not load (picture below), maybe there is something wrong in my pages .py files, I will try to figure out what is the issue.

image

Open up your network tab and see what is happening there.

@jinnyzor , just to give you a feedback. The VScode was printing this on the terminal:

WARNING: dev bundles requested with serve_locally=False.
This is not supported, switching to serve_locally=True

And in the browser console, this error:

Uncaught ReferenceError: DashRenderer is not defined
    at login:55:86

Nevertheless, I redid using your code ipsis litteris and it run with no errors.
However, the back and forward buttons issue persisted even locking the whole application.
Therefore, I stayed with your first method, and when I deployed it using Nginx, sometimes the buttons worked (most times don’t). I hope this issue gets some attention by Dash developers…

Anyway, thanks again for the valuable help.

@GitHunter0, thanks for the feedback.

Have you tried upgrading dash to 2.7.1? It just recently came out.

Yes, I tried dash 2.7.1 but the problem persists.

Do you still have the second dcc.Location in the layout?

No, in the first method I used just one dcc.Location and in the second method (locking the entire app) I used no dcc.Location

Hmm, can you put your app.py up?

Also, what browser are you using?

What do mean by “put the app.py up”?

I tested with Chrome, Firefox, Brave and Edge

Maybe a relevant detail is that sometimes it works at the start but as soon I log in, the buttons stop working and stay that way even after logging out.

EDIT: @jinnyzor , This forum for some reason does not allow new users to have more than 3 replies, so I no longer able to respond to you. The last detail that might be informative is that the URL correctly changes in the browser address bar, however it does not trigger the browser to go to the page. My app.py is pretty much identical to the one you provided. I can send to you via Github.

Post the app.py inside the preformatted text. See if there is anything else I can see.

It’s weird that it doesn’t work… Technically, the pages aren’t navigating, but sending post requests to the server to update the pages layout.

Hi nedned and everybody! This is valuable info! I’m really interested in getting this solution to work and of course I want the app to be completely secure and protected. Would it be possible to secure the callbacks if I include current_user.is_authenticated within every important callback? Then the _dash_update_component would not Output anything right?

Example:

@callback(Output(“page-1-content”, “children”), Input(“page-1-dropdown”, “value”))
def page_1_dropdown(value):
if current_user.is_authenticated:
return f’You have selected “{value}”’
else:
return dash.no_update

How would I go about doing the same thing to _dash_layout ?

Also, I tried to run the curl command you provided in my windows cmd but just got a lot of other stuff as Output. What did I do wrong… hmm? What is the best way of testing the security of your app?

Input:

curl “http://localhost:8050/_dash-update-component” -H “Content-Type: application/json” --data-raw “{“output”:“page-2-content.children”,“outputs”:{“id”:“page-2-content”,“property”:“children”},“inputs”:[{“id”:“page-2-radios”,“property”:“value”,“value”:“Orange”}],“changedPropIds”:[“page-2-radios.value”]}”

Output:
<!doctype html>

werkzeug.exceptions.BadRequest: 400 Bad Request: Failed to decode JSON object: Expecting property name enclosed in double quotes: line 1 column 2 (char 1) // Werkzeug Debugger etc...

Hello @Mackan,

Welcome to the community!

@nedned, was referencing a specific element and update from his own app with the curl request. The reason why this works is because he knows specifically what will generate a response from the server. (This is a replay of the request)

You can query your login status in the callbacks, which does take some work.

You can also do what I mention here, and use the before_request to lock down undesired post and get requests.

The absolute best way, that I’ve seen, and the way I do it at work. Is to have flask login flow completely outside of Dash. This does require some HTML experience to create the form, but then you can lock down the rest of the app easier.

The benefit of this is that your app will always have the user to reference, which makes things easier.

2 Likes

Thank you for your answer! :slight_smile:

Yes, I understand the curl command was specific to his own app, but it seems it is from the “example” radio buttons page, which I also had in the app I tried the curl command on.

I’m going to consider doing the flask login flow outside of dash, but this will take me some time to learn.

1 Like

@jinnyzor

It’s been a minute, but I have been playing with this recently to both lock down and ‘direct’ access to a dashboard. In my case, the entire dashboard is populated based on one value that is different for each user. Ideally, the login would send users to the same entry page, but passing each users unique value, resulting in completely different data for each user. Almost all of the login demos I’ve seen simply redirect- is there a way to pass data as well?

Also, can you point to any examples of an app that has the flask login flow outside of dash?

Could you possibly elaborate by what you mean with different data? Some people redirect?

I dont know of one off the top of my head, but, my original version of this was using a flask route for the login.

I’ll see if I can make an example of it again. I use it at work, but its all behind the login wall, so you wouldnt be able to tell. Haha. And it’s closed source. :wink:

(not sure why I can’t quote reply).

First Question: It is a multi-tab dashboard with a dropdown in the header. The dropdown is a list of ‘users.’ That data that is displayed changes depending on the user selected. I want to create a login with a password for each user that only displays that user’s data (e.g., a dropdown list with one value) and, hopefully, also an ‘admin’ login that displays the entire dropdown.

I’ll keep digging on the login flow question. I started with dash, so while flask is obviously somewhat familiar, there are still aspects of it I am trying to wrap my head around.

Yes, the benefit of having the login flow outside of the dash app, is that when the user first encounters the application, they are a verified user.

This in turn allows you to create custom navigation based upon the user’s level of permissions.

It in a multi-page layout, this is easier because you have the layouts as functions, which make it even easier to query the user’s login status.

@etonblue,

Here is a login flow that is outside of dash:

"""
 CREDIT: This code was originally adapted for Pages  based on Nader Elshehabi's  article:
   https://dev.to/naderelshehabi/securing-plotly-dash-using-flask-login-4ia2
   https://github.com/naderelshehabi/dash-flask-login

   This version is updated by Dash community member @jinnyzor For more info see:
   https://community.plotly.com/t/dash-app-pages-with-flask-login-flow-using-flask/69507

For other Authentication options see:
  Dash Enterprise:  https://dash.plotly.com/authentication#dash-enterprise-auth
  Dash Basic Auth:  https://dash.plotly.com/authentication#basic-auth

"""


import os
from flask import Flask, request, redirect, session, jsonify, url_for, render_template
from flask_login import login_user, LoginManager, UserMixin, logout_user, current_user

import dash
from dash import dcc, html, Input, Output, State, ALL



# Exposing the Flask Server to enable configuring it for logging in
server = Flask(__name__)

@server.before_request
def check_login():
    if request.method == 'GET':
        if request.path in ['/login', '/logout']:
            return
        if current_user:
            if current_user.is_authenticated:
                return
            else:
                for pg in dash.page_registry:
                    if request.path == dash.page_registry[pg]['path']:
                        session['url'] = request.url
        return redirect(url_for('login'))
    else:
        if current_user:
            if request.path == '/login' or current_user.is_authenticated:
                return
        return jsonify({'status':'401', 'statusText':'unauthorized access'})


@server.route('/login', methods=['POST', 'GET'])
def login(message=""):
    if request.method == 'POST':
        if request.form:
            username = request.form['username']
            password = request.form['password']
            if VALID_USERNAME_PASSWORD.get(username) is None:
                return """invalid username and/or password <a href='/login'>login here</a>"""
            if VALID_USERNAME_PASSWORD.get(username) == password:
                login_user(User(username))
                if 'url' in session:
                    if session['url']:
                        url = session['url']
                        session['url'] = None
                        return redirect(url) ## redirect to target url
                return redirect('/') ## redirect to home
            message = "invalid username and/or password"
    else:
        if current_user:
            if current_user.is_authenticated:
                return redirect('/')
    return render_template('login.html', message=message)

@server.route('/logout', methods=['GET'])
def logout():
    if current_user:
        if current_user.is_authenticated:
            logout_user()
    return render_template('login.html', message="you have been logged out")

app = dash.Dash(
    __name__, server=server, use_pages=True, suppress_callback_exceptions=True
)

# Keep this out of source code repository - save in a file or a database
#  passwords should be encrypted
VALID_USERNAME_PASSWORD = {"test": "test", "hello": "world"}


# Updating the Flask Server configuration with Secret Key to encrypt the user session cookie
server.config.update(SECRET_KEY=os.getenv("SECRET_KEY"))

# Login manager object will be used to login / logout users
login_manager = LoginManager()
login_manager.init_app(server)
login_manager.login_view = "/login"


class User(UserMixin):
    # User data model. It has to have at least self.id as a minimum
    def __init__(self, username):
        self.id = username


@login_manager.user_loader
def load_user(username):
    """This function loads the user by user id. Typically this looks up the user from a user database.
    We won't be registering or looking up users in this example, since we'll just login using LDAP server.
    So we'll simply return a User object with the passed in username.
    """
    return User(username)


app.layout = html.Div(
    [
        html.A('logout', href='../logout'),
        html.Br(),
        dash.page_container,
    ]
)


if __name__ == "__main__":
    app.run_server(debug=True)

You’ll need to create a new templates folder just like the pages.

templates/login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Flask Login Flow</title>
</head>
<body>
<form method="POST" action="/login">
    <div>Please login to continue:</div>
    <div><span style="color:red">{{message}}</span></div>
    <input placeholder="Enter your username" type="text" id="uname-box" name='username'>
    <input placeholder="Enter your password" type="password" id="pwd-box" name='password'>
    <Button type="submit" id="login-button">Login</Button>
</form>
</body>
</html>

This is a basic demonstration. If you try to navigate to a page without being logged in, you will be redirected to the login page, and your url saved.

If you are an authenticated user and try to navigate to login, you will be redirected to the home page.

Another benefit of this flow, is that it significantly reduces the complexity of the before_request that determines if it should cater a response.


Some things to always keep in mind:

  • flask routes take priority over dash routes.
  • As you make new paths available in the Flask side, you will need to make sure you add them as available in the before_request if they are not login required.
  • If you want to reference your assets/static folders, you will also need to allow those paths through.
1 Like

Wow. Thanks for the quick response, I’ll let you know how it goes!

1 Like

Thank you for your contributions jinnyzor, it is much appreciated!
Im have a dash app running on Azure and I’m trying to get this login method working with a user (postgres)database. I’m sure I’ll get there in the end, but do you have some basic example where this is applied by any chance?
So instead using the VALID_USERNAME_PASSWORD variable in the code, I would like to use the load_user function here.
Otherwise I’ll post something here when I get it working.

Cheers