How to secure a Dash app with dash-auth

In this tutorial, we’ll see how you can protect your Dash app with the package dash-auth.

We’ll cover how to set up basic authentication, how to define a custom authentication function, and even how to connect it to a database.

Let’s go! ⬇️

Basic Dash auth

First, install Dash and Dash Auth:

pip install dash dash-auth

Next, define a list of users and their associated passwords:

from dash import Dash, html
import dash_auth

# ⚠️ Defining passwords directly in code is a bad practice.
# Better: store them in a secrets file or a database.
USERS = {
    "alice": "secret123",
    "bob": "pa$$w0rd"
}

# Create the Dash app
app = Dash(__name__)

# Add authentication
auth = dash_auth.BasicAuth(
    app,
    username_password_list=USERS,
    # Set the secret key to something random.
    secret_key="something_like_nUGz8DZvb..."
)

# Define the layout
app.layout = html.Div([
    html.H1("Protected app"),
    html.P("If you see this, you are authenticated ✅")
])

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

The secret key must be unique and… secret 😉.
To generate one, run this in your terminal:

python -c "import secrets; print(secrets.token_urlsafe(32))"

Note: the secret_key isn’t mandatory for dash-auth since it doesn’t use cookies. However, adding it removes the warning: “WARNING:root:Session is not available. Have you set a secret key?”.

Then, just run your app with python app.py. You should get this result:

And after log-in:

That’s it! You get a protected page. 🤩

There are some limitations:

  • You cannot customize the login/password popup window, it’s natively handled by the browser.
  • Users cannot logout: the session is active until they close their browser.
  • The list of users is fixed: you will need to reload the app to add or remove a user or update a password.

Retrieve the user

The user information is stored in the flask.session object. It’s therefore easy to retrieve it in a callback:

# ...

# Define the layout
app.layout = html.Div([
    html.H1("Protected app"),
    html.P("If you see this, you are authenticated ✅"),
    html.P(id="user_info")  # added this
])

# Define the callback
@app.callback(
    Output("user_info", "children"),
    Input("user_info", "id"),  # dummy trigger
)
def update_user_info(_):
    """ Display the username of the logged in user. """
    username = flask.session.get("user").get("email")
    return f"You are logged in as {username}."

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

Let’s explain this code:

  • I added a new paragraph #user_info
  • A new callback is triggered at initialization time. It displays the username.

Now let’s see the result:

Authentication function

It’s possible to use an arbitrary python function to handle authentication. This is useful to:

  • store and compare hashed passwords instead of plain passwords;
  • retrieve user information and passwords from a database.

Comparing hashed passwords

Storing passwords in clear in the code is a pretty bad practice for security and confidentiality reasons. Instead, a better practice is to store hashed passwords instead of the clear version. If the password leaks, the original password can’t be found!

You can hash a password using the generate_password_hash from werkzeug.security package. For instance:

from werkzeug.security import generate_password_hash
print(generate_password_hash("secret123"))
# scrypt:32768:8:1$PICpPDH9JdIz75DT$d5f81a560015a407e51989f03d046816954ba2b8d5ee520b999f492352b9e8a39084210ea2fe3bfdcdc2a94047a5397e04bbf3663b505967d3d1ea91a95101d7'

Then, we can compare the two password hashes with the check_password_hash function:


from werkzeug.security import check_password_hash

def auth_func(username, password):
    """ Authenticate the user using hashed passwords. """

    # Check the user exists and the password is correct
    if username in USERS and check_password_hash(USERS[username], password):
        return True
    return False

That’s it. We just check the user exists, and compare its hash.

Let’s put this all together and replace the USERS list with the auth_func parameter:

from dash import Dash, html
from dash_auth import BasicAuth
from werkzeug.security import generate_password_hash, check_password_hash

# This time, we store hashed passwords. 
# Meaning that they can't be compromised if our code leaks.
USERS = {
    "alice": "scrypt:32768:8:1$HtB7iBw3ZGspBOHX$36879fd912db96322af2c33c3eb3fd0142ca2ec51988f332cc477b89c012449e5562487b410264b02d495d785780d0099e9130b42c4f61d7bc9166b93f7d1626",
    "bob": "scrypt:32768:8:1$j2rFpuXSr2D5t4Ij$9436a942b25f93054d3c96e54dcdd342c97f86515e09aafecd564984c22beb991029b70a89164215590ec131be4e13a12e043f4572ceb06576f86f925b1b9d65"
}

def auth_func(username, password):
    """ Authenticate the user using hashed passwords. """
    if username in USERS and check_password_hash(USERS[username], password):
        return True
    return False

app = Dash(__name__)
auth = BasicAuth(
    app, 
    auth_func=auth_func, # We now use auth_func in place of the user list.
    secret_key="something_like_nUGz8DZvb..."
)

app.layout = html.Div([
    html.H1("Protected app"),
    html.P("If you see this, you are authenticated.")
])

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

The result is visually the same for the user, but now passwords are not stored in clear. It also means that the original passwords are not shared with the developers having access to the Dash app code.

Much better! 🙂

Use a database

We can also use the authentication function to request an external database. The good thing with this solution is that the list of users can be dynamically managed.

Let’s create a simple script to initialize a sqlite database:

# init_db.py
import sqlite3
from werkzeug.security import generate_password_hash

DB_PATH = "users.db"

def init_db():
    """Create a tiny users table and seed two demo users if empty."""
    with sqlite3.connect(DB_PATH) as con:
        cur = con.cursor()
        cur.execute("""
            DROP TABLE IF EXISTS users;
            CREATE TABLE users (
                username TEXT PRIMARY KEY,
                password_hash TEXT NOT NULL,
                language TEXT NOT NULL
            )
        """)

        # Insert values
        cur.executemany(
            "INSERT INTO users(username, password_hash, language) VALUES (?, ?, ?)",
            [
                ("alice", generate_password_hash("secret123"), "en"),
                ("bob",   generate_password_hash("pa$$w0rd"), "fr"),
            ],
        )

if __name__ == "__main__":
    init_db()
    print("Database initialized")

Note that we directly save the hashed password in the users table.

Then, we simply query this database in our auth_func function to get the hashed password of a user, then compare it:

from dash import Dash, html
from dash_auth import BasicAuth
from werkzeug.security import generate_password_hash, check_password_hash
import sqlite3

DB_PATH = "users.db"

def get_hash(username):
    """Fetch the stored hash for a username, or None."""
    with sqlite3.connect(DB_PATH) as con:
        cur = con.cursor()
        cur.execute("SELECT password_hash FROM users WHERE username = ?", (username,))
        row = cur.fetchone()
        return row[0] if row else None

def auth_func(username, password):
    """ Authenticate the user using hashed passwords. """
    hash_from_db = get_hash(username)

    if hash_from_db and check_password_hash(hash_from_db, password):
        return True
    return False

app = Dash(__name__)
auth = BasicAuth(
    app, 
    auth_func=auth_func,
    secret_key="something_like_nUGz8DZvb..."
)

app.layout = html.Div([
    html.H1("Protected app"),
    html.P("If you see this, you are authenticated ✅"),
])

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

That’s it!

Now you can modify the list of users, modify passwords, etc. in the database without having to reload the application. 🙂

It doesn’t need to be a sqlite database. You can also make HTTP requests to a database provider, or query any other type of database (Postgresql, MongoDB, …).

Keep in mind that the auth_func function is executed for every callback and every dash request. If the check takes too much time, think about caching solutions like memoize.

Why you can’t logout

dash-auth uses HTTP Basic Authentication, which is built into your browser.

  • Your credentials (username + password) are cached.
  • The browser automatically re-sends them with every request.
  • There’s no “logout” button because the browser doesn’t expose a way to clear those credentials.

The only way to log out? Close the browser tab or window.

Conclusion

This tutorial helped you to secure a Dash app using the dash_auth package.

dash-auth has its drawbacks, but it’s a fairly easy solution to add an authentication layer to a dashboard or a data app. 🙂 However, I would not recommend using it for production apps that have very sensitive data.

If you need proper login/logout behavior (e.g., session expiration, multiple users switching), you’ll need a more advanced system such as:

  • flask_login
  • OAuth (Google, GitHub…)
  • Enterprise identity providers (Okta, Auth0, Azure AD, …)
  • Dash-Enterprise (handles LDAP, SAML, OIDC)

In the next tutorial, we will see how to use dash-auth for multi-pages Dash apps.

I hope to see you there!