Skip to main content

Dash Authorization Package.

Project description

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!",
)

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dash_auth_async-1.3.3.tar.gz (43.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dash_auth_async-1.3.3-py3-none-any.whl (41.1 kB view details)

Uploaded Python 3

File details

Details for the file dash_auth_async-1.3.3.tar.gz.

File metadata

  • Download URL: dash_auth_async-1.3.3.tar.gz
  • Upload date:
  • Size: 43.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for dash_auth_async-1.3.3.tar.gz
Algorithm Hash digest
SHA256 d1b463288deb2f134f0ee868f1d7ff30fc58c72f867b351dcbfe17bf9d2e9ff7
MD5 52c18adf7364a7dc647fabc692a23c22
BLAKE2b-256 cf56221ccea1be9640f592503755c95cacfb2f6c112bce8aa4d4bbc29ed0481a

See more details on using hashes here.

Provenance

The following attestation bundles were made for dash_auth_async-1.3.3.tar.gz:

Publisher: release.yaml on joschrag/dash-auth-async

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file dash_auth_async-1.3.3-py3-none-any.whl.

File metadata

  • Download URL: dash_auth_async-1.3.3-py3-none-any.whl
  • Upload date:
  • Size: 41.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for dash_auth_async-1.3.3-py3-none-any.whl
Algorithm Hash digest
SHA256 d8dd55404f3740f08d9070102c418a511bb9fcf7f5992736d4fca38c00c8baac
MD5 1b1dfbd3e1ae3ce8d0dea96f36b0e86b
BLAKE2b-256 5a8df3beabedcc6681acfbbadd96ad6bf35d18bfc5f205a0563df447c6559114

See more details on using hashes here.

Provenance

The following attestation bundles were made for dash_auth_async-1.3.3-py3-none-any.whl:

Publisher: release.yaml on joschrag/dash-auth-async

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page