Dash + gunicorn + flask authentication issues with rendered dashboards

I am having issues trying to find a solution for keeping the authentication of a user using plotly dash with flask and gunicorn. It seems that if another user queries the server or even after a while of jumping from one dashboard to the other, the session user information gets lost.

# app.py
import dash
import dash_bootstrap_components as dbc
from dash import html, dcc, Input, Output, State
from flask import Flask, session
from flask_session import Session
from redis import Redis
from datetime import timedelta
import os
import secrets

# Initialize the Flask server
server = Flask(__name__)
server.secret_key = os.environ.get('SECRET_KEY', secrets.token_urlsafe(16))

# Configure Flask-Session with Redis
server.config["SESSION_TYPE"] = "redis"
server.config["SESSION_PERMANENT"] = True
server.config["SESSION_USE_SIGNER"] = True
server.config["PERMANENT_SESSION_LIFETIME"] = timedelta(minutes=30)
server.config["SESSION_REDIS"] = Redis(host='localhost', port=6379)  # Adjust Redis host/port if needed

# Initialize Flask-Session
Session(server)

# Initialize the Dash app
app = dash.Dash(__name__, server=server, external_stylesheets=[dbc.themes.BOOTSTRAP], suppress_callback_exceptions=True)

# Layout for login modal and navbar
app.layout = html.Div([
    dcc.Location(id='url', refresh=True),
    html.Div(id='page-content'),
    dbc.Modal(
        [
            dbc.ModalHeader("Login"),
            dbc.ModalBody([
                dbc.Input(id='username', type='text', placeholder='Username'),
                dbc.Input(id='password', type='password', placeholder='Password'),
                dbc.Button('Login', id='login-button', color="primary")
            ]),
        ], id="login-modal", is_open=True
    ),
    dbc.NavbarSimple(
        children=[
            dbc.Button("Logout", id="logout-button", color="danger", className="ml-auto")
        ], brand="App", color="dark", dark=True, id='navbar', style={'display': 'none'}
    ),
])

# Simulated user database
USER_DATABASE = {
    'test_user': 'test_password',
    'test_user2': 'test_password'
}

@app.server.before_request
def refresh_session():
    session.modified = True  # Refresh session TTL on each request

# Callback to handle login and logout
@app.callback(
    [Output('login-modal', 'is_open'), Output('navbar', 'style')],
    [Input('login-button', 'n_clicks'), Input('logout-button', 'n_clicks')],
    [State('username', 'value'), State('password', 'value')],
    prevent_initial_call=True
)
def handle_login_logout(n_login, n_logout, username, password):
    # Login process
    if n_login and username in USER_DATABASE and USER_DATABASE[username] == password:
        session['user'] = {'username': username}  # Store user session
        return False, {'display': 'block'}  # Close login modal, show navbar

    # Logout process
    if n_logout:
        session.pop('user', None)  # Clear session
        return True, {'display': 'none'}  # Reopen login modal, hide navbar

    return dash.no_update, dash.no_update

# Redirect logic
@app.callback(
    Output('page-content', 'children'),
    [Input('url', 'pathname')]
)
def display_page(pathname):
    if 'user' in session:
        return html.Div([html.H1(f"Welcome, {session['user']['username']}"), html.P("This is your dashboard.")])
    return html.Div([html.H1("Please log in to access this page.")])

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

The following is a dashboard under ā€˜pagesā€™ directory:

$ home.py

import dash
import dash_bootstrap_components as dbc
from dash import html, dcc
from flask import session
import dash

dash.register_page(__name__)

def layout():
    # Check if user data is available in the session
    user = session.get('user')

    if not user:
        # Redirect to a login page or show a message if no user is found in session
        return html.Div("Please log in to access this page.")

    # Display the user's info pulled from the session
    return dbc.Container(fluid=True, children=[
        dbc.Row([
            dbc.Col([
                html.H2(f"Welcome, {user['username']}!"),
                html.P("This is your personal dashboard."),
                html.P(f"Email: {user['email']}"),
                dbc.Button("Logout", id="logout-button", color="danger", className="mt-3")
            ], width=6)
        ], justify="center")
    ])

Can someone please tell me how I can retain an individual users session information in order for me to ensure the user is authenticated and that I can pull user tailored data from a database, if I wanted to? The issue that I keep seeing is that the session stored data keeps being deleted if another user is querying anything from the server.

For social authentication look to Dash Enterprise: Dash Enterprise: The Premier Data App Platform for Python

For basic username and password these are some tips and my experience in getting that setup:

My approach was a bit different than what youā€™ve went with, going to give some useful context but not a full solution as authentication is a headache within itself and depending on the size of the project might be worth reaching out to the dash enterprise team and setting up a workspace with them.

Easiest solution:

also check out some of B3devā€™s work:

I didnā€™t take the easy route because of the unique nature of my setup but for a multi app authentication system Iā€™ve managed to get this working in two separate options, cookies and using dcc.Store. My stack is two separate applications, a dash application running in its own docker container and a django application running in its own docker container. All information associated with database, authentication and users is stored on django with dash acting solely as a front end UI application.

With cookies my approach was to setup a:

@server.route(ā€˜/authā€™)

@server.route(ā€œ/get-cookieā€)

@server.route(ā€œ/set-cookie//ā€)

along with some helper functions of:

def set_user_cookie(username, password):

def fetch_user_cookie():

and a callback of:

@app.callback(
    # Output("store", "data"), hiding the modal on success
    [
        Output("login-modal-form", "hidden"),
        # Output("User-Avatar", "children"),
        Output("url", "href"),
        Output("auth-store", "data"),
        # Output("welcome-back-alert", "children"),
        # Output('render-navbar-based-on-logged-in-status', 'children')
    ],
    [
        Input("login-button", "n_clicks"),
        Input("login-email", "value"),
        Input("login-password", "value"),
    ],
    State("auth-store", "data"),
    prevent_initial_call=True,
)
def get_data(login_button, email, password, user_state):
    if login_button is None:
        return dash.no_update
    elif login_button:
        if email and password is not None:
            print("initial store")
            print(user_state)
            login_test = login(email, password)
            if login_test:
                print("testing login")
                print(login_test)
                print(type(login_test))
                print()

                user_token = create_user_token(
                    username=f'{login_test["email"]}',
                    password=f"{password}",
                )

                print(
                    "**************************************************************************",
                )
                print(f'{True}, "/profile/", {user_token}')
                print(type(get_user_info(email=email)))
                print(get_user_info(email=email))
                print(
                    "**************************************************************************",
                )
                return (
                    True,
                    "/profile/",
                    user_token,
                )
            else:
                return dash.no_update
    else:
        return dash.no_update

I recently moved away from cookies based authentication as it wasnā€™t necessary for my specific project and went to setup authentication with dcc.Store which was much easier to get setup and working.

Basically my approach was:

server = Flask(__name__)
# load_dotenv()

# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

layout = html.Div([
dmc.NotificationProvider(position="top-right", zIndex=1010, style={'marginTop': '50px'}),
        html.Div(id="notifications-container"),
        dcc.Location(id='url', refresh=False),
        dcc.Store(
            id='auth-store',
            storage_type='session',
        ),
]) # also need to add forms for login but this is the basis for most other authentication for my setup


@app.callback(
    [Output('url', 'pathname', allow_duplicate=True),
     Output('user-email', 'label', allow_duplicate=True),
     Output('auth-store', 'data', allow_duplicate=True)],
    Output('modal-centered', 'opened', allow_duplicate=True),
    Output("notifications-container", "children", allow_duplicate=True),
    [Input('login-button', 'n_clicks'),
     Input('logout-button', 'n_clicks')],
    [State('email-input', 'value'),
     State('password-input', 'value'),
     State('auth-store', 'data')],
    prevent_initial_call=True
)
def handle_auth(login_clicks, logout_clicks, email, password, auth_data):
    ctx = callback_context

    if not ctx.triggered:
        raise PreventUpdate

    if ctx.triggered_id == 'login-button':
        try:
            response = requests.post(
                f'{DJANGO_API_URL}/user/login',
                data={'email': email, 'password': password}
            )

            if response.status_code == 200:
                data = response.json()
                new_auth_data = {
                    'access_token': data['access'],
                    'user_email': data['user']['email']
                }

                # Fetch user details
                headers = {'Authorization': f'Bearer {data["access"]}'}
                user_details_response = requests.get(
                    f'{DJANGO_API_URL}/account/details?user={data["user"]["email"]}',
                    headers=headers
                )

                if user_details_response.status_code == 200:
                    user_details = user_details_response.json()
                    new_auth_data['user_details'] = user_details

                return (
                    '/',
                    data['user']['email'],
                    new_auth_data,
                    False,
                    dmc.Notification(
                        title=f"Welcome back, {data['user']['email']}!",
                        id="login-notify",
                        action="show",
                        color="green",
                        autoClose=5000,
                        message="Successfully logged in to GeošŸ—ŗļøIndex",
                        icon=DashIconify(icon="mdi:human-welcome"),
                    )
                )
        except Exception as e:
            print(f"Login error: {e}")
            return dash.no_update

    elif ctx.triggered_id == 'logout-button':
        return (
            '/',
            'Login to your account',
            None,
            dash.no_update,
            dmc.Notification(
                title="Logged out successfully",
                id="logout-notify",
                action="show",
                color="red",
                autoClose=5000,
                message="šŸ‘‹ See you soon!",
                icon=DashIconify(icon="mdi:exit-run"),
            )
        )

    raise PreventUpdate

@app.callback(
    [Output('url', 'pathname', allow_duplicate=True),
     Output('user-email', 'label', allow_duplicate=True),
     Output('auth-store', 'data', allow_duplicate=True),
     Output("notifications-container", "children", allow_duplicate=True)],
    [Input('url', 'pathname')],
    [State('auth-store', 'data')],
    prevent_initial_call=True
)
def handle_page_load(pathname, auth_data):
    print(f"Page load - pathname: {pathname}, auth_data: {auth_data}")  # Debug log

    if not auth_data:
        return pathname, 'Login to your account', None, dash.no_update

    # Keep the user on the current page but authenticated
    if pathname == '/dashboard':
        return '/', auth_data['user_email'], auth_data, dash.no_update

    # Validate the token and refresh user details if needed
    if auth_data and 'access_token' in auth_data:
        try:
            headers = {'Authorization': f'Bearer {auth_data["access_token"]}'}
            # First verify if the token is still valid
            response = requests.get(
                f'{DJANGO_API_URL}/user/user-info',
                headers=headers
            )

            if response.status_code != 200:
                print("Token invalid or expired")
                return pathname, 'Login to your account', None, dash.no_update

            # If token is valid, get user details
            details_response = requests.get(
                f'{DJANGO_API_URL}/account/details?user={auth_data["user_email"]}',
                headers=headers
            )

            if details_response.status_code == 200:
                user_details = details_response.json()
                updated_auth_data = {
                    'access_token': auth_data['access_token'],
                    'user_email': auth_data['user_email'],
                    'user_details': user_details
                }
                return pathname, auth_data['user_email'], updated_auth_data, dash.no_update

        except Exception as e:
            print(f"Error during auth check: {e}")
            # On error, maintain existing auth state
            return pathname, auth_data['user_email'], auth_data, dash.no_update

    return pathname, auth_data['user_email'], auth_data, dash.no_update

This solution is based on having a backend user system that has the ability to setup access_tokens for account authentication. Both of these solutions are based on the user inputting a username and password to authenticate, havenā€™t been able to find a way to get social authentication from google or github loginā€¦ though Iā€™ve tried for many many hours :smiling_face_with_tear:

Some other tips in resolving issues with authentication.

  1. use logging import logging
  2. use graphana Grafana dashboards | Grafana Labs adding this tool to your stack will pay dividends in fully understanding whats taking place behind the scenes in deployment. I have a dashboard for both applications that returns constant logs while the app is deployed and its been super useful and affective with helping me resolve authentication issues

This seems related to the fact that you have mutiple threads or workers (I guess, as you mentionned gunicorn). Maybe the problem is that session is not shared between instances of your app?

Did you set your env variable SECRET_KEY ? If not, this code will generate a new key for each instance of your app. To be clear, if you have two workers specified with Gunicorn, that will generate 2 apps with 2 different SECRET_KEY. It might be the problem if it is not set.

I use flask_login and it does exactly this. And I experienced the exact same problem : I didnā€™t set the SECRET_KEY outside of my code. :slight_smile:

1 Like

Yes, I am using gunicorn. How do you set that relationship up correctly so that I can have, say 8 or 16 workers? The reason being that I might have a lot of users on the same site and I want to be able to tailor that dashboard towards their own data access.

so you are saying I should change my secret key only once and that would solve the issue with sessions from different users interfering/erasing the previous users session info? OOoooor are you saying that I need to create a secret key for every session so that it can handle one session dedicated to each user that logs in and not have their session data lost?

I was originally using flask_login but I encountered this issue. I then started looking into other methods of authentication that works with dash +flask + gunicorn. I am trying to learn as much as possible. Right now I am trying to find out whether flask_login or flask jwt extended JWTManager. I am worried about someone being able to hijack someone elseā€™s access credentials once my site goes public.

Hello @jarelan,

The flask secret key is what creates a cookie, this needs to be the same for the whole application and the same across all workers. Otherwise the application will log a user out if the secret key is not the same.

The secret key is just a way to encode and decode the cookie so that it is usable. However, never put any sensitive info in the cookie because it can be decoded quite easily. Just do a google search for the flask cookie decoder.

This secret key is very important, as people who gain access to this key will be able to create cookies that make them seem logged in, and can also elevate permissions via changing info inside of the cookie.

1 Like

So my plan was to use flask_login and have it verify the user by refreshing the users session each time they query the server. Otherwise, i was going to start using the JWTManager to create a dcc.store with the users information. That, youā€™re saying, would be bad?

As also said @jinnyzor, you should set one secret key that will be share by all instances of your app.

That means setting $SECRET_KEY in your environment. And I suggest changing the code to :

# SECRET KEY should be secret and set at the environment level, not in the code.
server.secret_key = os.environ['SECRET_KEY']

That way it will raise an error if the environment variable $SECRET_KEY is not set.


I think this modification will solve your initial problem.
Regarding the way to store user information, anything that you store in dcc.Store might be seen by the user and should be considered as a non-trustable user input.
Instead, you should store the user information in a database (Redis, SQLite, ā€¦) and retrieve the info each time thank to the session id. As this session id is created by Flask and uses your SECRET_KEY, nobody will be able to create an id on their own. As a result, they will not able to steal somebody else session and somebody else user info.

1 Like

I was already on that. Once you guys pointed it out I metaphorically slapped my forehead. I am setting the code up so that it sets the os environment variable if it is not there.

I am also starting to build a redis session db for the users session data.

Thank you for your advice. I really appreciate you helping me out. I am trying to learn as much as possible.

Youā€™re welcome. It took me some time to figure this out as well, these are not easy topics. :slight_smile:

Nor are there any real guides on these topics. It seems like these are kept secrets to give them advantages over othersā€¦or something like that.

Here is a topic and discussion on these things:

1 Like