Hello All!
I’ve been working to be able to use the Flask method of logging in. This is an alteration of the flask_login method provided here: dash-multi-page-app-demos/multi_page_flask_login at main · AnnMarieW/dash-multi-page-app-demos · GitHub
This will allow you to:
- restrict individual pages and redirect to login while saving the path (note: you will use the registered page name to restrict)
- once logged in, will redirect to the saved path or to the home page
- when logging out, it will redirect to the login screen after 3 seconds
This utilizes a form to submit a post request to the flask server which brings in the redirecting element. Obviously, you can alter this however you desire.
Here is the code:
app.py
"""
CREDIT: This code is 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
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
from flask_login import login_user, LoginManager, UserMixin, logout_user, current_user
import dash
from dash import dcc, html, Input, Output, State, ALL
from dash.exceptions import PreventUpdate
from utils.login_handler import restricted_page
# Exposing the Flask Server to enable configuring it for logging in
server = Flask(__name__)
@server.route('/login', methods=['POST'])
def login_button_click():
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
return """invalid username and/or password <a href='/login'>login here</a>"""
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(
[
dcc.Location(id="url"),
html.Div(id="user-status-header"),
html.Hr(),
dash.page_container,
]
)
@app.callback(
Output("user-status-header", "children"),
Output('url','pathname'),
Input("url", "pathname"),
Input({'index': ALL, 'type':'redirect'}, 'n_intervals')
)
def update_authentication_status(path, n):
### logout redirect
if n:
if not n[0]:
return '', dash.no_update
else:
return '', '/login'
### test if user is logged in
if current_user.is_authenticated:
if path == '/login':
return dcc.Link("logout", href="/logout"), '/'
return dcc.Link("logout", href="/logout"), dash.no_update
else:
### if page is restricted, redirect to login and save path
if path in restricted_page:
session['url'] = path
return dcc.Link("login", href="/login"), '/login'
### if path not login and logout display login link
if current_user and path not in ['/login', '/logout']:
return dcc.Link("login", href="/login"), dash.no_update
### if path login and logout hide links
if path in ['/login', '/logout']:
return '', dash.no_update
if __name__ == "__main__":
app.run_server(debug=True)
utils > login_handler.py
import dash
restricted_page = {}
def require_login(page):
for pg in dash.page_registry:
if page == pg:
restricted_page[dash.page_registry[pg]['path']] = True
pages > home.py
import dash
from dash import html, dcc
dash.register_page(__name__, path="/")
layout = html.Div(
[
dcc.Link("Go to Page 1", href="/page-1"),
html.Br(),
dcc.Link("Go to Page 2", href="/page-2"),
]
)
pages > login.py
import dash
from dash import html, dcc
dash.register_page(__name__)
# Login screen
layout = html.Form(
[
html.H2("Please log in to continue:", id="h1"),
dcc.Input(placeholder="Enter your username", type="text", id="uname-box", name='username'),
dcc.Input(placeholder="Enter your password", type="password", id="pwd-box", name='password'),
html.Button(children="Login", n_clicks=0, type="submit", id="login-button"),
html.Div(children="", id="output-state")
], method='POST'
)
pages > logout.py
import dash
from dash import html, dcc
from flask_login import logout_user, current_user
dash.register_page(__name__)
def layout():
if current_user.is_authenticated:
logout_user()
return html.Div(
[
html.Div(html.H2("You have been logged out - You will be redirected to login")),
dcc.Interval(id={'index':'redirectLogin', 'type':'redirect'}, n_intervals=0, interval=1*3000)
]
)
pages > page-1.py
import dash
from dash import html, dcc, Output, Input, callback
dash.register_page(__name__)
layout = html.Div(
[
html.H1("Page 1"),
dcc.Dropdown(
id="page-1-dropdown",
options=[{"label": i, "value": i} for i in ["LA", "NYC", "MTL"]],
value="LA",
),
html.Div(id="page-1-content"),
html.Br(),
dcc.Link("Go to Page 2", href="/page-2"),
html.Br(),
dcc.Link("Go back to home", href="/"),
]
)
@callback(Output("page-1-content", "children"), Input("page-1-dropdown", "value"))
def page_1_dropdown(value):
return f'You have selected "{value}"'
pages > page-2.py
import dash
from dash import html, dcc, Output, Input, callback
from flask_login import current_user
from utils.login_handler import require_login
dash.register_page(__name__)
require_login(__name__)
def layout():
if not current_user.is_authenticated:
return html.Div(["Please ", dcc.Link("login", href="/login"), " to continue"])
return html.Div(
[
html.H1("Page 2"),
dcc.RadioItems(
id="page-2-radios",
options=[{"label": i, "value": i} for i in ["Orange", "Blue", "Red"]],
value="Orange",
),
html.Div(id="page-2-content"),
html.Br(),
dcc.Link("Go to Page 1", href="/page-1"),
html.Br(),
dcc.Link("Go back to home", href="/"),
]
)
@callback(Output("page-2-content", "children"), Input("page-2-radios", "value"))
def page_2_radios(value):
return f'You have selected "{value}"'
Let me know if you guys have any questions, I’ll try to answer them. There may still be some issues around the use of back buttons properly redirecting to login, just make sure you protect your valuable content with current_user.is_authenticated.