diff --git a/CHANGELOG.md b/CHANGELOG.md index 0444d3c..92a0198 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [Unreleased] - 2025-08-15 +### Fixed +- Fix public routes being protected when passing `url_base_pathname` or `routes_pathname_prefix` to app +- Fix OIDC redirects after login and logout when passing `url_base_pathname` or `routes_pathname_prefix` to app + ## [2.3.0] - 2024-03-18 ### Added - OIDCAuth allows to authenticate via OIDC diff --git a/dash_auth/auth.py b/dash_auth/auth.py index 862bb5e..42c2c50 100644 --- a/dash_auth/auth.py +++ b/dash_auth/auth.py @@ -6,16 +6,16 @@ from flask import request from .public_routes import ( - add_public_routes, get_public_callbacks, get_public_routes + add_public_routes, + get_public_callbacks, + get_public_routes, + get_url_base, ) class Auth(ABC): def __init__( - self, - app: Dash, - public_routes: Optional[list] = None, - **obsolete + self, app: Dash, public_routes: Optional[list] = None, **obsolete ): """Auth base class for authentication in Dash. @@ -47,14 +47,15 @@ def _protect(self): @server.before_request def before_request_auth(): - public_routes = get_public_routes(self.app) public_callbacks = get_public_callbacks(self.app) + url_base = get_url_base(self.app) # Handle Dash's callback route: # * Check whether the callback is marked as public # * Check whether the callback is performed on route change in # which case the path should be checked against the public routes - if request.path == "/_dash-update-component": + callback_path = f"{url_base.rstrip('/')}/_dash-update-component" + if request.path == callback_path: body = request.get_json() # Check whether the callback is marked as public @@ -66,7 +67,8 @@ def before_request_auth(): # should be checked against the public routes pathname = next( ( - inp.get("value") for inp in body["inputs"] + inp.get("value") + for inp in body["inputs"] if isinstance(inp, dict) and inp.get("property") == "pathname" ), diff --git a/dash_auth/oidc_auth.py b/dash_auth/oidc_auth.py index 86e6c7e..dbe948d 100644 --- a/dash_auth/oidc_auth.py +++ b/dash_auth/oidc_auth.py @@ -7,12 +7,14 @@ from authlib.integrations.base_client import OAuthError from authlib.integrations.flask_client import OAuth from dash_auth.auth import Auth +from dash_auth.public_routes import get_url_base from flask import Response, redirect, request, session, url_for from werkzeug.routing import Map, Rule if TYPE_CHECKING: from authlib.integrations.flask_client.apps import ( - FlaskOAuth1App, FlaskOAuth2App + FlaskOAuth1App, + FlaskOAuth2App, ) @@ -62,6 +64,15 @@ def __init__( callback_route : str, optional The route for the OIDC redirect URI, it requires a placeholder, by default "/oidc//callback". + + NOTE: login_route, logout_route, and callback_route are + registered directly on the Flask server at the paths given here, + regardless of any ``url_base_pathname`` or + ``routes_pathname_prefix`` set on the Dash app. If your app is + deployed under a prefix (e.g. ``url_base_pathname="/app/"``), the + OIDC routes still live at the server root (e.g. + ``/oidc//callback``), NOT under the prefix. Configure your + IDP's redirect URI accordingly. idp_selection_route : str, optional The route for the IDP selection function, by default None log_signins : bool, optional @@ -102,8 +113,7 @@ def __init__( app.server.secret_key = secret_key if app.server.secret_key is None: - raise RuntimeError( - """ + raise RuntimeError(""" app.server.secret_key is missing. Generate a secret key in your Python session with the following commands: @@ -116,8 +126,7 @@ def __init__( Note that you should not do this dynamically: you should create a key and then assign the value of that key in your code/via a secret. - """ - ) + """) if secure_session: app.server.config["SESSION_COOKIE_SECURE"] = True @@ -175,9 +184,7 @@ def register_provider(self, idp_name: str, **kwargs): ) client_kwargs = kwargs.pop("client_kwargs", {}) client_kwargs.setdefault("scope", "openid email") - self.oauth.register( - idp_name, client_kwargs=client_kwargs, **kwargs - ) + self.oauth.register(idp_name, client_kwargs=client_kwargs, **kwargs) def get_oauth_client(self, idp: str): """Get the OAuth client.""" @@ -194,9 +201,7 @@ def get_oauth_kwargs(self, idp: str): if idp not in self.oauth._registry: raise ValueError(f"'{idp}' is not a valid registered idp") - kwargs: dict = ( - self.oauth._registry[idp][1] - ) + kwargs: dict = self.oauth._registry[idp][1] return kwargs def _create_redirect_uri(self, idp: str): @@ -242,7 +247,7 @@ def login_request(self, idp: str = None): def logout(self): # pylint: disable=C0116 """Logout the user.""" session.clear() - base_url = self.app.config.get("url_base_pathname") or "/" + base_url = get_url_base(self.app) or "/" page = self.logout_page or f"""
@@ -269,7 +274,7 @@ def callback(self, idp: str): # pylint: disable=C0116 user = token.get("userinfo") return self.after_logged_in(user, idp, token) - def after_logged_in(self, user: Optional[dict], idp: str, token: dict): + def after_logged_in(self, user: Optional[dict], idp: str, token: dict): """ Post-login actions after successful OIDC authentication. For example, allows to pass custom attributes to the user session: @@ -288,7 +293,7 @@ def after_logged_in(self, user, idp, token): if self.log_signins: logging.info("User %s is logging in.", user.get("email")) - return redirect(self.app.config.get("url_base_pathname") or "/") + return redirect(get_url_base(self.app) or "/") def is_authorized(self): # pylint: disable=C0116 """Check whether ther user is authenticated.""" diff --git a/dash_auth/public_routes.py b/dash_auth/public_routes.py index 5c9540c..09ac736 100644 --- a/dash_auth/public_routes.py +++ b/dash_auth/public_routes.py @@ -6,7 +6,6 @@ from dash import get_app from werkzeug.routing import Map, MapAdapter, Rule - DASH_PUBLIC_ASSETS_EXTENSIONS = "js,css" BASE_PUBLIC_ROUTES = [ f"/assets/.{ext}" @@ -25,6 +24,26 @@ PUBLIC_CALLBACKS = "PUBLIC_CALLBACKS" +def get_url_base(app: Dash) -> str: + """Return the URL prefix configured for the Dash app (e.g. '/app/'). + + Returns '' when no prefix is configured. Checks url_base_pathname first, + then requests_pathname_prefix, then routes_pathname_prefix. In normal Dash + usage these three values are always kept in sync by Dash itself; the + fallback order only matters in advanced deployments. + + This reads from app.config at call time, so it must be invoked after the + Dash app's URL config is fully initialised. In particular, calling + add_public_routes() before url_base_pathname is set on the app will store + routes without the prefix and they will never match at request time. + """ + return ( + app.config.get("url_base_pathname") + or app.config.get("routes_pathname_prefix") + or "" + ) + + def add_public_routes(app: Dash, routes: list): """Add routes to the public routes list. @@ -48,11 +67,14 @@ def add_public_routes(app: Dash, routes: list): """ public_routes = get_public_routes(app) + url_base = get_url_base(app) if not public_routes.map._rules: routes = BASE_PUBLIC_ROUTES + routes for route in routes: + if url_base and not route.startswith(url_base): + route = url_base.rstrip("/") + route public_routes.map.add(Rule(route)) app.server.config[PUBLIC_ROUTES] = public_routes @@ -72,16 +94,17 @@ def decorator(func): wrapped_func = callback(*callback_args, **callback_kwargs)(func) callback_id = next( ( - k for k, v in GLOBAL_CALLBACK_MAP.items() + k + for k, v in GLOBAL_CALLBACK_MAP.items() if inspect.getsource(v["callback"]) == inspect.getsource(func) ), None, ) try: app = get_app() - app.server.config[PUBLIC_CALLBACKS] = ( - get_public_callbacks(app) + [callback_id] - ) + app.server.config[PUBLIC_CALLBACKS] = get_public_callbacks(app) + [ + callback_id + ] except Exception: print( "Could not set up the public callback as the Dash object " diff --git a/tests/test_basic_auth_integration.py b/tests/test_basic_auth_integration.py index 1a6a534..572d138 100644 --- a/tests/test_basic_auth_integration.py +++ b/tests/test_basic_auth_integration.py @@ -1,26 +1,31 @@ from dash import Dash, Input, Output, dcc, html import requests - +import pytest from dash_auth import BasicAuth, add_public_routes, protected - TEST_USERS = { - "valid": [ - ["hello", "world"], - ["hello2", "wo:rld"] - ], - "invalid": [ - ["hello", "password"] - ], + "valid": [["hello", "world"], ["hello2", "wo:rld"]], + "invalid": [["hello", "password"]], } -def test_ba001_basic_auth_login_flow(dash_br, dash_thread_server): - app = Dash(__name__) - app.layout = html.Div([ - dcc.Input(id="input", value="initial value"), - html.Div(id="output") - ]) +@pytest.mark.parametrize( + "kwargs", + [ + {}, + {"url_base_pathname": "/app/"}, + {"url_base_pathname": "/sub/app/"}, + { + "routes_pathname_prefix": "/app/", + "requests_pathname_prefix": "/app/", + }, + ], +) +def test_ba001_basic_auth_login_flow(dash_br, dash_thread_server, kwargs): + app = Dash(__name__, **kwargs) + app.layout = html.Div( + [dcc.Input(id="input", value="initial value"), html.Div(id="output")] + ) @app.callback(Output("output", "children"), Input("input", "value")) def update_output(new_value): @@ -30,15 +35,25 @@ def update_output(new_value): add_public_routes(app, ["/user//public"]) dash_thread_server(app) - base_url = dash_thread_server.url + path_prefix = ( + app.config.get("url_base_pathname", "") + or app.config.get("requests_pathname_prefix", "") + or app.config.get("routes_pathname_prefix", "") + ) + base_url = dash_thread_server.url + path_prefix def test_failed_views(url): assert requests.get(url).status_code == 401 def test_successful_views(url): - assert requests.get(url.strip("/") + "/_dash-layout").status_code == 200 - assert requests.get(url.strip("/") + "/home").status_code == 200 - assert requests.get(url.strip("/") + "/user/john123/public").status_code == 200 + assert ( + requests.get(url.rstrip("/") + "/_dash-layout").status_code == 200 + ) + assert requests.get(url.rstrip("/") + "/home").status_code == 200 + assert ( + requests.get(url.rstrip("/") + "/user/john123/public").status_code + == 200 + ) test_failed_views(base_url) test_successful_views(base_url) @@ -60,12 +75,23 @@ def test_successful_views(url): dash_br.wait_for_text_to_equal("#output", "initial value") -def test_ba002_basic_auth_groups(dash_br, dash_thread_server): - app = Dash(__name__) - app.layout = html.Div([ - dcc.Input(id="input", value="initial value"), - html.Div(id="output") - ]) +@pytest.mark.parametrize( + "kwargs", + [ + {}, + {"url_base_pathname": "/app/"}, + {"url_base_pathname": "/sub/app/"}, + { + "routes_pathname_prefix": "/app/", + "requests_pathname_prefix": "/app/", + }, + ], +) +def test_ba002_basic_auth_groups(dash_br, dash_thread_server, kwargs): + app = Dash(__name__, **kwargs) + app.layout = html.Div( + [dcc.Input(id="input", value="initial value"), html.Div(id="output")] + ) @app.callback( Output("output", "children"), @@ -89,7 +115,12 @@ def update_output(new_value): ) dash_thread_server(app) - base_url = dash_thread_server.url + path_prefix = ( + app.config.get("url_base_pathname", "") + or app.config.get("requests_pathname_prefix", "") + or app.config.get("routes_pathname_prefix", "") + ) + base_url = dash_thread_server.url + path_prefix for user, password in TEST_USERS["valid"]: # login using the URL instead of the alert popup diff --git a/tests/test_basic_auth_integration_auth_func.py b/tests/test_basic_auth_integration_auth_func.py index 653770e..364d9a9 100644 --- a/tests/test_basic_auth_integration_auth_func.py +++ b/tests/test_basic_auth_integration_auth_func.py @@ -5,13 +5,8 @@ from dash_auth import basic_auth TEST_USERS = { - "valid": [ - ["hello", "world"], - ["hello2", "wo:rld"] - ], - "invalid": [ - ["hello", "password"] - ], + "valid": [["hello", "world"], ["hello2", "wo:rld"]], + "invalid": [["hello", "password"]], } @@ -25,10 +20,9 @@ def auth_function(username, password): def test_ba002_basic_auth_login_flow(dash_br, dash_thread_server): app = Dash(__name__) - app.layout = html.Div([ - dcc.Input(id="input", value="initial value"), - html.Div(id="output") - ]) + app.layout = html.Div( + [dcc.Input(id="input", value="initial value"), html.Div(id="output")] + ) @app.callback(Output("output", "children"), Input("input", "value")) def update_output(new_value): @@ -41,7 +35,9 @@ def update_output(new_value): def test_failed_views(url): assert requests.get(url).status_code == 401 - assert requests.get(url.strip("/") + "/_dash-layout").status_code == 401 + assert ( + requests.get(url.strip("/") + "/_dash-layout").status_code == 401 + ) test_failed_views(base_url) @@ -64,10 +60,9 @@ def test_failed_views(url): # Test incorrect initialization of BasicAuth def both_dict_and_func(dash_br, dash_thread_server): app = Dash(__name__) - app.layout = html.Div([ - dcc.Input(id="input", value="initial value"), - html.Div(id="output") - ]) + app.layout = html.Div( + [dcc.Input(id="input", value="initial value"), html.Div(id="output")] + ) basic_auth.BasicAuth(app, TEST_USERS["valid"], auth_func=auth_function) return True @@ -75,10 +70,9 @@ def both_dict_and_func(dash_br, dash_thread_server): def both_no_auth_func_or_dict(dash_br, dash_thread_server): app = Dash(__name__) - app.layout = html.Div([ - dcc.Input(id="input", value="initial value"), - html.Div(id="output") - ]) + app.layout = html.Div( + [dcc.Input(id="input", value="initial value"), html.Div(id="output")] + ) basic_auth.BasicAuth(app) return True diff --git a/tests/test_group_protection.py b/tests/test_group_protection.py index f8be30d..4da9de3 100644 --- a/tests/test_group_protection.py +++ b/tests/test_group_protection.py @@ -6,22 +6,34 @@ def test_gp001_list_groups(): app = Flask(__name__) app.secret_key = "Test!" with app.test_request_context("/", method="GET"): - session["user"] = {"email": "a.b@mail.com", "groups": ["default"], "tenant": "ABC"} + session["user"] = { + "email": "a.b@mail.com", + "groups": ["default"], + "tenant": "ABC", + } assert list_groups() == ["default"] - assert list_groups(groups_key="tenant", groups_str_split=",") == ["ABC"] + assert list_groups(groups_key="tenant", groups_str_split=",") == [ + "ABC" + ] def test_gp002_check_groups(): app = Flask(__name__) app.secret_key = "Test!" with app.test_request_context("/", method="GET"): - session["user"] = {"email": "a.b@mail.com", "groups": ["default"], "tenant": "ABC"} + session["user"] = { + "email": "a.b@mail.com", + "groups": ["default"], + "tenant": "ABC", + } assert check_groups(["default"]) is True assert check_groups(["other"]) is False assert check_groups(["default", "other"]) is True assert check_groups(["other", "default"], check_type="all_of") is False assert check_groups(["default"], check_type="all_of") is True - assert check_groups(["other", "default"], check_type="none_of") is False + assert ( + check_groups(["other", "default"], check_type="none_of") is False + ) assert check_groups(["other"], check_type="none_of") is True @@ -33,7 +45,11 @@ def func(): return "success" with app.test_request_context("/", method="GET"): - session["user"] = {"email": "a.b@mail.com", "groups": ["default"], "tenant": "ABC"} + session["user"] = { + "email": "a.b@mail.com", + "groups": ["default"], + "tenant": "ABC", + } f0 = protected( unauthenticated_output="unauthenticated", missing_permissions_output="forbidden", diff --git a/tests/test_oidc_auth.py b/tests/test_oidc_auth.py index 5442a67..afda02f 100644 --- a/tests/test_oidc_auth.py +++ b/tests/test_oidc_auth.py @@ -1,4 +1,3 @@ -import os from unittest.mock import patch import requests @@ -9,6 +8,10 @@ protected_callback, OIDCAuth, ) +import pytest + +_FLASK_OAUTH_MODULE = "authlib.integrations.flask_client.apps.FlaskOAuth2App" +_METADATA_URL = "https://idp2.com/oidc/2/.well-known/openid-configuration" def valid_authorize_redirect(_, redirect_uri, *args, **kwargs): @@ -17,7 +20,9 @@ def valid_authorize_redirect(_, redirect_uri, *args, **kwargs): def invalid_authorize_redirect(_, redirect_uri, *args, **kwargs): base_url = "/" + redirect_uri.split("/", maxsplit=3)[-1] - return redirect(f"{base_url}?error=Unauthorized&error_description=something went wrong") + return redirect( + f"{base_url}?error=Unauthorized&error_description=something went wrong" + ) def valid_authorize_access_token(*args, **kwargs): @@ -27,18 +32,40 @@ def valid_authorize_access_token(*args, **kwargs): } -@patch("authlib.integrations.flask_client.apps.FlaskOAuth2App.authorize_redirect", valid_authorize_redirect) -@patch("authlib.integrations.flask_client.apps.FlaskOAuth2App.authorize_access_token", valid_authorize_access_token) -def test_oa001_oidc_auth_login_flow_success(dash_br, dash_thread_server): - app = Dash(__name__) - app.layout = html.Div([ - dcc.Input(id="input", value="initial value"), - html.Div(id="output1"), - html.Div(id="output2"), - html.Div("static", id="output3"), - html.Div("static", id="output4"), - html.Div("not static", id="output5"), - ]) +@pytest.mark.parametrize( + "kwargs", + [ + {}, + {"url_base_pathname": "/app/"}, + {"url_base_pathname": "/sub/app/"}, + { + "routes_pathname_prefix": "/app/", + "requests_pathname_prefix": "/app/", + }, + ], +) +@patch( + f"{_FLASK_OAUTH_MODULE}.authorize_redirect", + valid_authorize_redirect, +) +@patch( + f"{_FLASK_OAUTH_MODULE}.authorize_access_token", + valid_authorize_access_token, +) +def test_oa001_oidc_auth_login_flow_success( + dash_br, dash_thread_server, kwargs +): + app = Dash(__name__, **kwargs) + app.layout = html.Div( + [ + dcc.Input(id="input", value="initial value"), + html.Div(id="output1"), + html.Div(id="output2"), + html.Div("static", id="output3"), + html.Div("static", id="output4"), + html.Div("not static", id="output5"), + ] + ) @app.callback(Output("output1", "children"), Input("input", "value")) def update_output1(new_value): @@ -86,10 +113,15 @@ def update_output5(new_value): token_endpoint_auth_method="client_secret_post", client_id="", client_secret="", - server_metadata_url="https://idp.com/oidc/2/.well-known/openid-configuration", + server_metadata_url=_METADATA_URL, ) dash_thread_server(app) - base_url = dash_thread_server.url + path_prefix = ( + app.config.get("url_base_pathname", "") + or app.config.get("requests_pathname_prefix", "") + or app.config.get("routes_pathname_prefix", "") + ) + base_url = dash_thread_server.url + path_prefix assert requests.get(base_url).status_code == 200 @@ -101,13 +133,27 @@ def update_output5(new_value): dash_br.wait_for_text_to_equal("#output5", "initial value") -@patch("authlib.integrations.flask_client.apps.FlaskOAuth2App.authorize_redirect", invalid_authorize_redirect) -def test_oa002_oidc_auth_login_fail(dash_thread_server): - app = Dash(__name__) - app.layout = html.Div([ - dcc.Input(id="input", value="initial value"), - html.Div(id="output") - ]) +@pytest.mark.parametrize( + "kwargs", + [ + {}, + {"url_base_pathname": "/app/"}, + {"url_base_pathname": "/sub/app/"}, + { + "routes_pathname_prefix": "/app/", + "requests_pathname_prefix": "/app/", + }, + ], +) +@patch( + f"{_FLASK_OAUTH_MODULE}.authorize_redirect", + invalid_authorize_redirect, +) +def test_oa002_oidc_auth_login_fail(dash_thread_server, kwargs): + app = Dash(__name__, **kwargs) + app.layout = html.Div( + [dcc.Input(id="input", value="initial value"), html.Div(id="output")] + ) @app.callback(Output("output", "children"), Input("input", "value")) def update_output(new_value): @@ -119,10 +165,15 @@ def update_output(new_value): token_endpoint_auth_method="client_secret_post", client_id="", client_secret="", - server_metadata_url="https://idp.com/oidc/2/.well-known/openid-configuration", + server_metadata_url=_METADATA_URL, ) dash_thread_server(app) - base_url = dash_thread_server.url + path_prefix = ( + app.config.get("url_base_pathname", "") + or app.config.get("requests_pathname_prefix", "") + or app.config.get("routes_pathname_prefix", "") + ) + base_url = dash_thread_server.url + path_prefix def test_unauthorized(url): r = requests.get(url) @@ -133,17 +184,39 @@ def test_authorized(url): assert requests.get(url).status_code == 200 test_unauthorized(base_url) - test_authorized(os.path.join(base_url, "public")) - - -@patch("authlib.integrations.flask_client.apps.FlaskOAuth2App.authorize_redirect", valid_authorize_redirect) -@patch("authlib.integrations.flask_client.apps.FlaskOAuth2App.authorize_access_token", valid_authorize_access_token) -def test_oa003_oidc_auth_login_several_idp(dash_br, dash_thread_server): - app = Dash(__name__) - app.layout = html.Div([ - dcc.Input(id="input", value="initial value"), - html.Div(id="output1"), - ]) + test_authorized(base_url.rstrip("/") + "/public") + + +@pytest.mark.parametrize( + "kwargs", + [ + {}, + {"url_base_pathname": "/app/"}, + {"url_base_pathname": "/sub/app/"}, + { + "routes_pathname_prefix": "/app/", + "requests_pathname_prefix": "/app/", + }, + ], +) +@patch( + f"{_FLASK_OAUTH_MODULE}.authorize_redirect", + valid_authorize_redirect, +) +@patch( + f"{_FLASK_OAUTH_MODULE}.authorize_access_token", + valid_authorize_access_token, +) +def test_oa003_oidc_auth_login_several_idp( + dash_br, dash_thread_server, kwargs +): + app = Dash(__name__, **kwargs) + app.layout = html.Div( + [ + dcc.Input(id="input", value="initial value"), + html.Div(id="output1"), + ] + ) @app.callback(Output("output1", "children"), Input("input", "value")) def update_output1(new_value): @@ -156,7 +229,7 @@ def update_output1(new_value): token_endpoint_auth_method="client_secret_post", client_id="", client_secret="", - server_metadata_url="https://idp.com/oidc/2/.well-known/openid-configuration", + server_metadata_url=_METADATA_URL, ) # Add a second provider oidc.register_provider( @@ -164,25 +237,32 @@ def update_output1(new_value): token_endpoint_auth_method="client_secret_post", client_id="", client_secret="", - server_metadata_url="https://idp2.com/oidc/2/.well-known/openid-configuration", + server_metadata_url=_METADATA_URL, ) dash_thread_server(app) + path_prefix = ( + app.config.get("url_base_pathname", "") + or app.config.get("requests_pathname_prefix", "") + or app.config.get("routes_pathname_prefix", "") + ) base_url = dash_thread_server.url - + base_url_prefix = (base_url + path_prefix).rstrip("/") assert requests.get(base_url).status_code == 400 + # Also verify that the prefixed app URL itself requires auth selection. + assert requests.get(base_url_prefix).status_code == 400 # Login with IDP1 - assert requests.get(os.path.join(base_url, "oidc/idp1/login")).status_code == 200 + assert requests.get(base_url + "/oidc/idp1/login").status_code == 200 # Logout - assert requests.get(os.path.join(base_url, "oidc/logout")).status_code == 200 + assert requests.get(base_url + "/oidc/logout").status_code == 200 assert requests.get(base_url).status_code == 400 # Login with IDP2 - assert requests.get(os.path.join(base_url, "oidc/idp2/login")).status_code == 200 + assert requests.get(base_url + "/oidc/idp2/login").status_code == 200 - dash_br.driver.get(os.path.join(base_url, "oidc/idp2/login")) - dash_br.driver.get(base_url) + dash_br.driver.get(base_url + "/oidc/idp2/login") + dash_br.driver.get(base_url_prefix) dash_br.wait_for_text_to_equal("#output1", "initial value") diff --git a/usage.py b/usage.py index 72b6e20..9bd76e7 100644 --- a/usage.py +++ b/usage.py @@ -2,10 +2,8 @@ import dash_auth # Keep this out of source code repository - save in a file or a database -VALID_USERNAME_PASSWORD_PAIRS = { - 'hello': 'world' -} - +VALID_USERNAME_PASSWORD_PAIRS = {"hello": "world"} +SECRET = "Test!" # Authorization function defined by developer # (can be used instead of VALID_USERNAME_PASSWORD_PAIRS [Example 2 below]) @@ -16,42 +14,42 @@ def authorization_function(username, password): return False -external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] +external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"] app = Dash(__name__, external_stylesheets=external_stylesheets) # Example 1 (using username/password map) -auth = dash_auth.BasicAuth(app, VALID_USERNAME_PASSWORD_PAIRS) +auth = dash_auth.BasicAuth( + app, VALID_USERNAME_PASSWORD_PAIRS, secret_key=SECRET +) # Example 2 (using authorization function) # auth = dash_auth.BasicAuth(app, auth_func=authorization_function) -app.layout = html.Div([ - html.H1('Welcome to the app'), - html.H3('You are successfully authorized'), - dcc.Dropdown( - id='dropdown', - options=[{'label': i, 'value': i} for i in ['A', 'B']], - value='A' - ), - dcc.Graph(id='graph') -], className="container") - - -@app.callback(Output('graph', 'figure'), [Input('dropdown', 'value')]) +app.layout = html.Div( + [ + html.H1("Welcome to the app"), + html.H3("You are successfully authorized"), + dcc.Dropdown( + id="dropdown", + options=[{"label": i, "value": i} for i in ["A", "B"]], + value="A", + ), + dcc.Graph(id="graph"), + ], + className="container", +) + + +@app.callback(Output("graph", "figure"), [Input("dropdown", "value")]) def update_graph(dropdown_value): return { - 'layout': { - 'title': 'Graph of {}'.format(dropdown_value), - 'margin': { - 'l': 20, - 'b': 20, - 'r': 10, - 't': 60 - } + "layout": { + "title": "Graph of {}".format(dropdown_value), + "margin": {"l": 20, "b": 20, "r": 10, "t": 60}, }, - 'data': [{'x': [1, 2, 3], 'y': [4, 1, 2]}] + "data": [{"x": [1, 2, 3], "y": [4, 1, 2]}], } -if __name__ == '__main__': - app.run_server(debug=True) +if __name__ == "__main__": + app.run(debug=True)