Skip to content

Commit 58bdcb7

Browse files
authored
Merge pull request #143 from michaels0m/master
Adding user defined authentication function as parameter
2 parents c39f714 + 10bb5fc commit 58bdcb7

File tree

6 files changed

+156
-12
lines changed

6 files changed

+156
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
77
## [Unreleased]
88
### Changed
99
- Uses flask `before_request` to protect all endpoints rather than protecting routes present at instantiation time
10+
- Allows user to use user-defined authorization python function instead of a dictionary/list of usernames and passwords
1011

1112
## [2.0.0] - 2023-03-10
1213
### Removed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,20 @@ USER_PWD = {
4141
}
4242
BasicAuth(app, USER_PWD)
4343
```
44+
45+
One can also use an authorization python function instead of a dictionary/list of usernames and passwords:
46+
47+
```python
48+
from dash import Dash
49+
from dash_auth import BasicAuth
50+
51+
def authorization_function(username, password):
52+
if (username == "hello") and (password == "world"):
53+
return True
54+
else:
55+
return False
56+
57+
58+
app = Dash(__name__)
59+
BasicAuth(app, auth_func = authorization_function)
60+
```

dash_auth/basic_auth.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import base64
2-
from typing import Union
2+
from typing import Union, Callable
33
import flask
44
from dash import Dash
55

@@ -10,20 +10,35 @@ class BasicAuth(Auth):
1010
def __init__(
1111
self,
1212
app: Dash,
13-
username_password_list: Union[list, dict],
13+
username_password_list: Union[list, dict] = None,
14+
auth_func: Callable = None
1415
):
1516
"""Add basic authentication to Dash.
1617
1718
:param app: Dash app
1819
:param username_password_list: username:password list, either as a
1920
list of tuples or a dict
21+
:param auth_func: python function accepting two string
22+
arguments (username, password) and returning a
23+
boolean (True if the user has access otherwise False).
2024
"""
2125
Auth.__init__(self, app)
22-
self._users = (
23-
username_password_list
24-
if isinstance(username_password_list, dict)
25-
else {k: v for k, v in username_password_list}
26-
)
26+
self._auth_func = auth_func
27+
if self._auth_func is not None:
28+
if username_password_list is not None:
29+
raise ValueError("BasicAuth can only use authorization "
30+
"function (auth_func kwarg) or "
31+
"username_password_list, it cannot use both.")
32+
else:
33+
if username_password_list is None:
34+
raise ValueError("BasicAuth requires username/password map "
35+
"or user-defined authorization function.")
36+
else:
37+
self._users = (
38+
username_password_list
39+
if isinstance(username_password_list, dict)
40+
else {k: v for k, v in username_password_list}
41+
)
2742

2843
def is_authorized(self):
2944
header = flask.request.headers.get('Authorization', None)
@@ -32,7 +47,14 @@ def is_authorized(self):
3247
username_password = base64.b64decode(header.split('Basic ')[1])
3348
username_password_utf8 = username_password.decode('utf-8')
3449
username, password = username_password_utf8.split(':', 1)
35-
return self._users.get(username) == password
50+
if self._auth_func is not None:
51+
try:
52+
return self._auth_func(username, password)
53+
except Exception as e:
54+
print(e)
55+
return False
56+
else:
57+
return self._users.get(username) == password
3658

3759
def login_request(self):
3860
return flask.Response(

dev-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ dash[testing]>=2
22
requests[security]
33
flake8
44
flask
5+
pytest
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
from dash import Dash, Input, Output, dcc, html
2+
import requests
3+
import pytest
4+
5+
from dash_auth import basic_auth
6+
7+
TEST_USERS = {
8+
"valid": [
9+
["hello", "world"],
10+
["hello2", "wo:rld"]
11+
],
12+
"invalid": [
13+
["hello", "password"]
14+
],
15+
}
16+
17+
18+
# Test using auth_func instead of TEST_USERS directly
19+
def auth_function(username, password):
20+
if [username, password] in TEST_USERS["valid"]:
21+
return True
22+
else:
23+
return False
24+
25+
26+
def test_ba002_basic_auth_login_flow(dash_br, dash_thread_server):
27+
app = Dash(__name__)
28+
app.layout = html.Div([
29+
dcc.Input(id="input", value="initial value"),
30+
html.Div(id="output")
31+
])
32+
33+
@app.callback(Output("output", "children"), Input("input", "value"))
34+
def update_output(new_value):
35+
return new_value
36+
37+
basic_auth.BasicAuth(app, auth_func=auth_function)
38+
39+
dash_thread_server(app)
40+
base_url = dash_thread_server.url
41+
42+
def test_failed_views(url):
43+
assert requests.get(url).status_code == 401
44+
assert requests.get(url.strip("/") + "/_dash-layout").status_code == 401
45+
46+
test_failed_views(base_url)
47+
48+
for user, password in TEST_USERS["invalid"]:
49+
test_failed_views(base_url.replace("//", f"//{user}:{password}@"))
50+
51+
# Test login for each user:
52+
for user, password in TEST_USERS["valid"]:
53+
# login using the URL instead of the alert popup
54+
# selenium has no way of accessing the alert popup
55+
dash_br.driver.get(base_url.replace("//", f"//{user}:{password}@"))
56+
57+
# the username:password@host url doesn"t work right now for dash
58+
# routes, but it saves the credentials as part of the browser.
59+
# visiting the page again will use the saved credentials
60+
dash_br.driver.get(base_url)
61+
dash_br.wait_for_text_to_equal("#output", "initial value")
62+
63+
64+
# Test incorrect initialization of BasicAuth
65+
def both_dict_and_func(dash_br, dash_thread_server):
66+
app = Dash(__name__)
67+
app.layout = html.Div([
68+
dcc.Input(id="input", value="initial value"),
69+
html.Div(id="output")
70+
])
71+
72+
basic_auth.BasicAuth(app, TEST_USERS["valid"], auth_func=auth_function)
73+
return True
74+
75+
76+
def both_no_auth_func_or_dict(dash_br, dash_thread_server):
77+
app = Dash(__name__)
78+
app.layout = html.Div([
79+
dcc.Input(id="input", value="initial value"),
80+
html.Div(id="output")
81+
])
82+
basic_auth.BasicAuth(app)
83+
return True
84+
85+
86+
def test_ba003_basic_auth_login_flow(dash_br, dash_thread_server):
87+
with pytest.raises(ValueError):
88+
both_dict_and_func(dash_br, dash_thread_server)
89+
with pytest.raises(ValueError):
90+
both_no_auth_func_or_dict(dash_br, dash_thread_server)
91+
return

usage.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,24 @@
66
'hello': 'world'
77
}
88

9+
10+
# Authorization function defined by developer
11+
# (can be used instead of VALID_USERNAME_PASSWORD_PAIRS [Example 2 below])
12+
def authorization_function(username, password):
13+
if (username == "hello") and (password == "world"):
14+
return True
15+
else:
16+
return False
17+
18+
919
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
1020
app = Dash(__name__, external_stylesheets=external_stylesheets)
11-
auth = dash_auth.BasicAuth(
12-
app,
13-
VALID_USERNAME_PASSWORD_PAIRS
14-
)
21+
22+
# Example 1 (using username/password map)
23+
auth = dash_auth.BasicAuth(app, VALID_USERNAME_PASSWORD_PAIRS)
24+
25+
# Example 2 (using authorization function)
26+
# auth = dash_auth.BasicAuth(app, auth_func=authorization_function)
1527

1628
app.layout = html.Div([
1729
html.H1('Welcome to the app'),

0 commit comments

Comments
 (0)