Dash App Pages with Flask Login Flow using Flask

@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