Skip to main content

Dash Authorization and Login

Lint Typecheck codecov Ruff ty PyPI Python License

Maintained by joschrag. Forked from plotly/dash-auth with the goal to add support for the new 4.x dash backends.

License: MIT

Feature overview

How this fork compares to upstream dash-auth. The headline difference is async: upstream is Flask-only, while dash-auth-async adds async callback support, the Quart and FastAPI backends, authenticated WebSocket callbacks, and a pluggable backend abstraction.

Capability dash-auth (upstream) dash-auth-async
Flask backend
Quart backend
FastAPI backend
Custom backends 1
Protected / public callbacks
Async callbacks 2
Authenticated WebSocket callbacks 3

✅ supported · ❌ not supported

1 detect_backend resolves Flask/Quart/FastAPI automatically; any other server is supported by supplying your own Backend instance.

2 Supported on every backend, Flask included — you don't need Quart or FastAPI, just the async extra (bundled as dash-auth-async[async], which pulls in dash[async]).

3 Provided by the Quart and FastAPI backends only. WebSocket auth is a no-op on Flask, which has no WebSocket callback transport.

For local testing, install uv, then install the dev dependencies and run individual tests:

uv sync
uv run pytest -k ba001

Note that Python 3.10 or greater is required.

Usage

Basic Authentication

To add basic authentication, add the following to your Dash app:

from dash import Dash
from dash_auth_async import BasicAuth

app = Dash(__name__)
USER_PWD = {
    "username": "password",
    "user2": "useSomethingMoreSecurePlease",
}
BasicAuth(app, USER_PWD)

One can also use an authorization python function instead of a dictionary/list of usernames and passwords:

from dash import Dash
from dash_auth_async import BasicAuth

def authorization_function(username, password):
    if (username == "hello") and (password == "world"):
        return True
    else:
        return False


app = Dash(__name__)
BasicAuth(app, auth_func = authorization_function)

Public routes

You can whitelist routes from authentication with the add_public_routes utility function, or by passing a public_routes argument to the Auth constructor. The public routes should follow Flask's route syntax.

from dash import Dash
from dash_auth_async import BasicAuth, add_public_routes

app = Dash(__name__)
USER_PWD = {
    "username": "password",
    "user2": "useSomethingMoreSecurePlease",
}
BasicAuth(app, USER_PWD, public_routes=["/"])

add_public_routes(app, public_routes=["/user/<user_id>/public"])

NOTE: If you are using server-side callbacks on your public routes, you should also use dash_auth_async's new public_callback rather than the default Dash callback. Below is an example of a public route and callbacks on a multi-page Dash app using Dash's pages API:

app.py

from dash import Dash, html, dcc, page_container
from dash_auth_async import BasicAuth

app = Dash(__name__, use_pages=True, suppress_callback_exceptions=True)
USER_PWD = {
    "username": "password",
    "user2": "useSomethingMoreSecurePlease",
}
BasicAuth(app, USER_PWD, public_routes=["/", "/user/<user_id>/public"])

app.layout = html.Div(
    [
        html.Div(
            [
                dcc.Link("Home", href="/"),
                dcc.Link("John Doe", href="/user/john_doe/public"),
            ],
            style={"display": "flex", "gap": "1rem", "background": "lightgray", "padding": "0.5rem 1rem"},
        ),
        page_container,
    ],
    style={"display": "flex", "flexDirection": "column"},
)

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

pages/home.py

from dash import Input, Output, html, register_page
from dash_auth_async import public_callback

register_page(__name__, "/")

layout = [
    html.H1("Home Page"),
    html.Button("Click me", id="home-button"),
    html.Div(id="home-contents"),
]

# Note the use of public callback here rather than the default Dash callback
@public_callback(
    Output("home-contents", "children"),
    Input("home-button", "n_clicks"),
)
def home(n_clicks):
    if not n_clicks:
        return "You haven't clicked the button."
    return "You clicked the button {} times".format(n_clicks)

pages/public_user.py

from dash import html, dcc, register_page

register_page(__name__, path_template="/user/<user_id>/public")

def layout(user_id: str):
    return [
        html.H1(f"User {user_id} (public)"),
        dcc.Link("Authenticated user content", href=f"/user/{user_id}/private"),
    ]

pages/private_user.py

from dash import html, register_page

register_page(__name__, path_template="/user/<user_id>/private")

def layout(user_id: str):
    return [
        html.H1(f"User {user_id} (authenticated only)"),
        html.Div("Members-only information"),
    ]

OIDC Authentication

To add authentication with OpenID Connect, you will first need to set up an OpenID Connect provider (IDP). This typically requires creating

  • An application in your IDP
  • Defining the redirect URI for your application, for testing locally you can use http://localhost:8050/oidc/callback
  • A client ID and secret for the application

Once you have set up your IDP, you can add it to your Dash app as follows:

from dash import Dash
from dash_auth_async import OIDCAuth

app = Dash(__name__)

auth = OIDCAuth(app, secret_key="aStaticSecretKey!")
auth.register_provider(
    "idp",
    token_endpoint_auth_method="client_secret_post",
    # Replace the below values with your own
    # NOTE: Do not hardcode your client secret!
    client_id="<my-client-id>",
    client_secret="<my-client-secret>",
    server_metadata_url="<my-idp-.well-known-configuration>",
)

Once this is done, connecting to your app will automatically redirect to the IDP login page.

Running behind a reverse proxy

Behavior change. Earlier versions rewrote the OIDC callback host from the client-supplied X-Forwarded-Host header. That header trust was removed — an attacker could set it to steer the redirect_uri handed to your IDP (auth-code leak / open redirect). The redirect_uri is now built only from the host and scheme of the incoming request.

The consequence: behind a proxy that terminates TLS or rewrites the host, the redirect_uri comes out as http://localhost:8050/... and your IDP rejects it. To get the real public host and scheme back, restore it at the transport layer — where the trust can be bounded to your actual proxy, instead of trusting any client.

Two separate things to restore: scheme (http→https) and host. Most app servers forward the scheme but not the host — --forwarded-allow-ips / --proxy-headers only governs X-Forwarded-Proto/-For, never the host. So:

  • Scheme: set force_https_callback=True on OIDCAuth (or pass the name of an env var that, when present, enables it). This covers the http→https half on every backend.
  • Host: add proxy middleware that honours X-Forwarded-Host, sized to your real proxy-chain length:
Backend Server(s) How to restore the host
Flask gunicorn, waitress, mod_wsgi, … werkzeug ProxyFix on the WSGI app (server-independent — gunicorn alone won't do the host)
Quart hypercorn ProxyFixMiddleware(mode="modern", trusted_hops=N) — restores host and scheme
Quart / FastAPI uvicorn uvicorn --proxy-headers does scheme only; add an X-Forwarded-Host ASGI middleware or have the proxy rewrite the Host header

Flask (WSGI — gunicorn, waitress, …):

from werkzeug.middleware.proxy_fix import ProxyFix

# Counts MUST equal your real proxy-chain length — setting them higher than the
# actual chain (or enabling this with no proxy) re-opens the spoofing hole.
app.server.wsgi_app = ProxyFix(app.server.wsgi_app, x_for=1, x_proto=1, x_host=1)

Quart (served by hypercorn) — wrap the ASGI app:

from hypercorn.middleware import ProxyFixMiddleware

# trusted_hops MUST equal your real proxy-chain length (see warning below).
app.server.asgi_app = ProxyFixMiddleware(app.server.asgi_app, mode="modern", trusted_hops=1)

FastAPI / Quart on uvicorn: uvicorn restores the scheme (--proxy-headers --forwarded-allow-ips=...) but not the host. Either have your proxy overwrite the Host header with the public host, or add a small trusted ASGI middleware that copies X-Forwarded-Host onto the host header — same trusted_hops discipline as above.

⚠️ Only restore proxy-header trust when a real proxy sets those headers, and set the hop counts to the actual chain length. A trusted-host middleware in front of no proxy lets a client spoof the host again — which is exactly the trust that was removed.

Multiple OIDC Providers

For multiple OIDC providers, you can use register_provider to add new ones after the OIDCAuth has been instantiated.

from dash import Dash, html
from dash_auth_async import OIDCAuth
from flask import request, redirect, url_for

app = Dash(__name__)

app.layout = html.Div([
    html.Div("Hello world!"),
    html.A("Logout", href="/oidc/logout"),
])

auth = OIDCAuth(
    app,
    secret_key="aStaticSecretKey!",
    # Set the route at which the user will select the IDP they wish to login with
    idp_selection_route="/login",
)
auth.register_provider(
    "IDP 1",
    token_endpoint_auth_method="client_secret_post",
    client_id="<my-client-id>",
    client_secret="<my-client-secret>",
    server_metadata_url="<my-idp-.well-known-configuration>",
)
auth.register_provider(
    "IDP 2",
    token_endpoint_auth_method="client_secret_post",
    client_id="<my-client-id2>",
    client_secret="<my-client-secret2>",
    server_metadata_url="<my-idp2-.well-known-configuration>",
)

@app.server.route("/login", methods=["GET", "POST"])
def login_handler():
    if request.method == "POST":
        idp = request.form.get("idp")
    else:
        idp = request.args.get("idp")

    if idp is not None:
        return redirect(url_for("oidc_login", idp=idp))

    return """<div>
        <form>
            <div>How do you wish to sign in:</div>
            <select name="idp">
                <option value="IDP 1">IDP 1</option>
                <option value="IDP 2">IDP 2</option>
            </select>
            <input type="submit" value="Login">
        </form>
    </div>"""


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

Async backends

Known Limitations

⚠️ WebSocket callbacks & auth: Do not enable websocket_callbacks=True globally on an authenticated use_pages app. The global flag routes every callback — including Dash's built-in page-routing callback — over the WebSocket, which bypasses the HTTP before_request auth guard where the login challenge is issued. Navigating to a protected page then hangs (the socket closes with 4401 and reconnect-loops) instead of prompting for login — the prompt appears only after a full page reload. Opt individual streaming callbacks into websocket=True instead, so routing and login stay on HTTP.

Quart (async) Backend

dash-auth-async supports Dash's Quart backend for fully async request handling. Install the quart extra to pull in the required dependencies:

pip install dash-auth-async[quart]

Then pass backend="quart" when creating your Dash app. The auth setup is identical to the Flask examples above — no code changes required beyond the backend flag.

BasicAuth with Quart
from dash import Dash
from dash_auth_async import BasicAuth

app = Dash(__name__, backend="quart")

BasicAuth(app, {"admin": "admin", "viewer": "viewer123"})

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=8050, debug=True)
OIDCAuth with Quart
import os
from dash import Dash, html
from dash_auth_async import OIDCAuth

app = Dash(__name__, backend="quart")

app.layout = html.Div([
    html.H2("OIDCAuth + Quart"),
    html.A("Logout", href="/oidc/logout"),
])

auth = OIDCAuth(app, secret_key="aStaticSecretKey!")
auth.register_provider(
    "myidp",
    client_id=os.environ["OIDC_CLIENT_ID"],
    client_secret=os.environ["OIDC_CLIENT_SECRET"],
    server_metadata_url=os.environ["OIDC_METADATA_URL"],
    token_endpoint_auth_method="client_secret_post",
    client_kwargs={"scope": "openid email profile"},
)

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=8050, debug=True)

Note: The Quart backend requires Dash >= 4.2.0 and Python >= 3.10.

FastAPI (async) Backend

dash-auth-async supports Dash's FastAPI backend too. Install the fastapi extra to pull in the required dependencies:

pip install dash-auth-async[fastapi]

Then pass backend="fastapi" when creating your Dash app. BasicAuth and OIDCAuth work exactly as on Flask/Quart — no code changes beyond the backend flag.

BasicAuth with FastAPI
from dash import Dash
from dash_auth_async import BasicAuth

app = Dash(__name__, backend="fastapi")

BasicAuth(
    app,
    {"admin": "admin", "viewer": "viewer123"},
    secret_key="aStaticSecretKey!",  # enables sessions (SessionMiddleware)
)

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=8050, debug=True)
OIDCAuth with FastAPI
import os
from dash import Dash, html
from dash_auth_async import OIDCAuth

app = Dash(__name__, backend="fastapi")

app.layout = html.Div([
    html.H2("OIDCAuth + FastAPI"),
    html.A("Logout", href="/oidc/logout"),
])

auth = OIDCAuth(app, secret_key="aStaticSecretKey!")
auth.register_provider(
    "myidp",
    client_id=os.environ["OIDC_CLIENT_ID"],
    client_secret=os.environ["OIDC_CLIENT_SECRET"],
    server_metadata_url=os.environ["OIDC_METADATA_URL"],
    token_endpoint_auth_method="client_secret_post",
    client_kwargs={"scope": "openid email profile"},
)

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=8050, debug=True)

Notes:

  • A secret_key installs Starlette's SessionMiddleware automatically. If you add your own SessionMiddleware, dash-auth-async defers to it.
  • Auth/OIDCAuth must be constructed before the server starts serving (Starlette forbids adding middleware after startup) — the normal usage pattern.
  • OIDC uses authlib's official starlette_client; no extra client module required.

The FastAPI backend requires Dash >= 4.2.0 and Python >= 3.10.

User-group-based permissions

dash_auth_async provides a convenient way to secure parts of your app based on user groups.

The following utilities are defined:

  • list_groups: Returns the groups of the current user, or None if the user is not authenticated.
  • check_groups: Checks the current user groups against the provided list of groups. Available group checks are one_of, all_of and none_of. The function returns None if the user is not authenticated.
  • protected: A function decorator that modifies the output if the user is unauthenticated or missing group permission.
  • protected_callback: A callback that only runs if the user is authenticated and with the right group permissions.

NOTE: user info is stored in the session so make sure you define a secret_key on the server to use this feature.

If you wish to use this feature with BasicAuth, you will need to define the groups for individual basicauth users:

from dash_auth_async import BasicAuth

app = Dash(__name__)
USER_PWD = {
    "username": "password",
    "user2": "useSomethingMoreSecurePlease",
}
BasicAuth(
    app,
    USER_PWD,
    user_groups={"username": ["group1", "group2"], "user2": ["group2"]},
    secret_key="Test!",
)

# You can also use a function to get user groups
def check_user(username, password):
    if username == "user1" and password == "password":
        return True
    if username == "user2" and password == "useSomethingMoreSecurePlease":
        return True
    return False

def get_user_groups(user):
    if user == "user1":
        return ["group1", "group2"]
    elif user == "user2":
        return ["group2"]
    return []

BasicAuth(
    app,
    auth_func=check_user,
    user_groups=get_user_groups,
    secret_key="Test!",
)

Release files for dash-auth-async 1.3.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dash-auth-async 1.3.4
File Size Uploaded
dash_auth_async-1.3.4.tar.gz 43.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for dash-auth-async 1.3.4
File Interpreter ABI Platform
dash_auth_async-1.3.4-py3-none-any.whl Python 3 none any Details

Total release size:84.6 kB

Release files / dash_auth_async-1.3.4.tar.gz

Download URL dash_auth_async-1.3.4.tar.gz
Size 43.6 kB
Tags Source
SHA-256 checksum
How to use checksums
8aab2a5bf283346422b9f3a57f8621229af1b452e645326f6467a25681d79346
BLAKE2b-256 checksum
How to use checksums
448ba6d2c4ed5a3d6ace8c3a97ffbced21241a45ce090b95e2e96af5d274e489
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 7, 2026.

Transparency log

Release files / dash_auth_async-1.3.4-py3-none-any.whl

Download URL dash_auth_async-1.3.4-py3-none-any.whl
Size 41.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e548debf50dc5ca066c303c7a9176aa8bee7ccb9fbc588f5ca0793e461e907c0
BLAKE2b-256 checksum
How to use checksums
187be59c7d421ee19d953d43b7960c7c095263d336e66bb0e6050880044dc543
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 7, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.3.4 This release

2 release files

1.3.3

2 release files

1.3.2

2 release files

1.3.1

2 release files

1.3.0

2 release files

1.2.1

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page