dash-auth-async 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dash_auth_async-1.0.0/LICENSE +22 -0
- dash_auth_async-1.0.0/MANIFEST.in +1 -0
- dash_auth_async-1.0.0/PKG-INFO +348 -0
- dash_auth_async-1.0.0/README.md +306 -0
- dash_auth_async-1.0.0/dash_auth_async/__init__.py +24 -0
- dash_auth_async-1.0.0/dash_auth_async/auth.py +94 -0
- dash_auth_async-1.0.0/dash_auth_async/basic_auth.py +113 -0
- dash_auth_async-1.0.0/dash_auth_async/group_protection.py +214 -0
- dash_auth_async-1.0.0/dash_auth_async/oidc_auth.py +333 -0
- dash_auth_async-1.0.0/dash_auth_async/public_routes.py +128 -0
- dash_auth_async-1.0.0/dash_auth_async/version.py +1 -0
- dash_auth_async-1.0.0/dash_auth_async.egg-info/PKG-INFO +348 -0
- dash_auth_async-1.0.0/dash_auth_async.egg-info/SOURCES.txt +20 -0
- dash_auth_async-1.0.0/dash_auth_async.egg-info/dependency_links.txt +1 -0
- dash_auth_async-1.0.0/dash_auth_async.egg-info/requires.txt +8 -0
- dash_auth_async-1.0.0/dash_auth_async.egg-info/top_level.txt +1 -0
- dash_auth_async-1.0.0/pyproject.toml +72 -0
- dash_auth_async-1.0.0/setup.cfg +4 -0
- dash_auth_async-1.0.0/tests/test_basic_auth_integration.py +130 -0
- dash_auth_async-1.0.0/tests/test_basic_auth_integration_auth_func.py +83 -0
- dash_auth_async-1.0.0/tests/test_group_protection.py +64 -0
- dash_auth_async-1.0.0/tests/test_oidc_auth.py +264 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 Plotly, Inc.
|
|
4
|
+
Copyright (c) 2025 Jonas Schrage
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include README.md
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dash-auth-async
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Dash Authorization Package.
|
|
5
|
+
Author-email: Jonas Schrage <119843859+joschrag@users.noreply.github.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/joschrag/dash-auth-async
|
|
8
|
+
Project-URL: Original project, https://github.com/plotly/dash-auth
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Framework :: Flask
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Education
|
|
14
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
15
|
+
Classifier: Intended Audience :: Healthcare Industry
|
|
16
|
+
Classifier: Intended Audience :: Manufacturing
|
|
17
|
+
Classifier: Intended Audience :: Science/Research
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Programming Language :: Python
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
26
|
+
Classifier: Topic :: Database :: Front-Ends
|
|
27
|
+
Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
|
|
28
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
29
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
30
|
+
Classifier: Topic :: Software Development :: Widget Sets
|
|
31
|
+
Requires-Python: >=3.10
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
License-File: LICENSE
|
|
34
|
+
Requires-Dist: authlib>=1.7.2
|
|
35
|
+
Requires-Dist: dash>=2
|
|
36
|
+
Requires-Dist: flask>=3.1.3
|
|
37
|
+
Requires-Dist: requests[security]>=2.34.2
|
|
38
|
+
Requires-Dist: werkzeug>=3.1.8
|
|
39
|
+
Provides-Extra: oidc
|
|
40
|
+
Requires-Dist: authlib; extra == "oidc"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
## Dash Authorization and Login
|
|
44
|
+
|
|
45
|
+
Maintained by [joschrag](https://github.com/joschrag/). Forked from [plotly/dash-auth](https://github.com/plotly/dash-auth) with the goal to add support for the new 4.x dash backends.
|
|
46
|
+
|
|
47
|
+
License: MIT
|
|
48
|
+
|
|
49
|
+
For local testing, install [uv](https://docs.astral.sh/uv/getting-started/installation/), then install the dev dependencies and run individual tests:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
uv sync
|
|
53
|
+
uv run pytest -k ba001
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Note that Python 3.10 or greater is required.
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
### Basic Authentication
|
|
61
|
+
|
|
62
|
+
To add basic authentication, add the following to your Dash app:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from dash import Dash
|
|
66
|
+
from dash_auth_async import BasicAuth
|
|
67
|
+
|
|
68
|
+
app = Dash(__name__)
|
|
69
|
+
USER_PWD = {
|
|
70
|
+
"username": "password",
|
|
71
|
+
"user2": "useSomethingMoreSecurePlease",
|
|
72
|
+
}
|
|
73
|
+
BasicAuth(app, USER_PWD)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
One can also use an authorization python function instead of a dictionary/list of usernames and passwords:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from dash import Dash
|
|
80
|
+
from dash_auth_async import BasicAuth
|
|
81
|
+
|
|
82
|
+
def authorization_function(username, password):
|
|
83
|
+
if (username == "hello") and (password == "world"):
|
|
84
|
+
return True
|
|
85
|
+
else:
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
app = Dash(__name__)
|
|
90
|
+
BasicAuth(app, auth_func = authorization_function)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Public routes
|
|
94
|
+
|
|
95
|
+
You can whitelist routes from authentication with the `add_public_routes` utility function,
|
|
96
|
+
or by passing a `public_routes` argument to the Auth constructor.
|
|
97
|
+
The public routes should follow [Flask's route syntax](https://flask.palletsprojects.com/en/2.3.x/quickstart/#routing).
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from dash import Dash
|
|
101
|
+
from dash_auth_async import BasicAuth, add_public_routes
|
|
102
|
+
|
|
103
|
+
app = Dash(__name__)
|
|
104
|
+
USER_PWD = {
|
|
105
|
+
"username": "password",
|
|
106
|
+
"user2": "useSomethingMoreSecurePlease",
|
|
107
|
+
}
|
|
108
|
+
BasicAuth(app, USER_PWD, public_routes=["/"])
|
|
109
|
+
|
|
110
|
+
add_public_routes(app, public_routes=["/user/<user_id>/public"])
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
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.
|
|
114
|
+
Below is an example of a public route and callbacks on a multi-page Dash app using Dash's pages API:
|
|
115
|
+
|
|
116
|
+
*app.py*
|
|
117
|
+
```python
|
|
118
|
+
from dash import Dash, html, dcc, page_container
|
|
119
|
+
from dash_auth_async import BasicAuth
|
|
120
|
+
|
|
121
|
+
app = Dash(__name__, use_pages=True, suppress_callback_exceptions=True)
|
|
122
|
+
USER_PWD = {
|
|
123
|
+
"username": "password",
|
|
124
|
+
"user2": "useSomethingMoreSecurePlease",
|
|
125
|
+
}
|
|
126
|
+
BasicAuth(app, USER_PWD, public_routes=["/", "/user/<user_id>/public"])
|
|
127
|
+
|
|
128
|
+
app.layout = html.Div(
|
|
129
|
+
[
|
|
130
|
+
html.Div(
|
|
131
|
+
[
|
|
132
|
+
dcc.Link("Home", href="/"),
|
|
133
|
+
dcc.Link("John Doe", href="/user/john_doe/public"),
|
|
134
|
+
],
|
|
135
|
+
style={"display": "flex", "gap": "1rem", "background": "lightgray", "padding": "0.5rem 1rem"},
|
|
136
|
+
),
|
|
137
|
+
page_container,
|
|
138
|
+
],
|
|
139
|
+
style={"display": "flex", "flexDirection": "column"},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
app.run(debug=True)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
*pages/home.py*
|
|
148
|
+
```python
|
|
149
|
+
from dash import Input, Output, html, register_page
|
|
150
|
+
from dash_auth_async import public_callback
|
|
151
|
+
|
|
152
|
+
register_page(__name__, "/")
|
|
153
|
+
|
|
154
|
+
layout = [
|
|
155
|
+
html.H1("Home Page"),
|
|
156
|
+
html.Button("Click me", id="home-button"),
|
|
157
|
+
html.Div(id="home-contents"),
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
# Note the use of public callback here rather than the default Dash callback
|
|
161
|
+
@public_callback(
|
|
162
|
+
Output("home-contents", "children"),
|
|
163
|
+
Input("home-button", "n_clicks"),
|
|
164
|
+
)
|
|
165
|
+
def home(n_clicks):
|
|
166
|
+
if not n_clicks:
|
|
167
|
+
return "You haven't clicked the button."
|
|
168
|
+
return "You clicked the button {} times".format(n_clicks)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
*pages/public_user.py*
|
|
173
|
+
```python
|
|
174
|
+
from dash import html, dcc, register_page
|
|
175
|
+
|
|
176
|
+
register_page(__name__, path_template="/user/<user_id>/public")
|
|
177
|
+
|
|
178
|
+
def layout(user_id: str):
|
|
179
|
+
return [
|
|
180
|
+
html.H1(f"User {user_id} (public)"),
|
|
181
|
+
dcc.Link("Authenticated user content", href=f"/user/{user_id}/private"),
|
|
182
|
+
]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
*pages/private_user.py*
|
|
187
|
+
```python
|
|
188
|
+
from dash import html, register_page
|
|
189
|
+
|
|
190
|
+
register_page(__name__, path_template="/user/<user_id>/private")
|
|
191
|
+
|
|
192
|
+
def layout(user_id: str):
|
|
193
|
+
return [
|
|
194
|
+
html.H1(f"User {user_id} (authenticated only)"),
|
|
195
|
+
html.Div("Members-only information"),
|
|
196
|
+
]
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### OIDC Authentication
|
|
200
|
+
|
|
201
|
+
To add authentication with OpenID Connect, you will first need to set up an OpenID Connect provider (IDP).
|
|
202
|
+
This typically requires creating
|
|
203
|
+
* An application in your IDP
|
|
204
|
+
* Defining the redirect URI for your application, for testing locally you can use http://localhost:8050/oidc/callback
|
|
205
|
+
* A client ID and secret for the application
|
|
206
|
+
|
|
207
|
+
Once you have set up your IDP, you can add it to your Dash app as follows:
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
from dash import Dash
|
|
211
|
+
from dash_auth_async import OIDCAuth
|
|
212
|
+
|
|
213
|
+
app = Dash(__name__)
|
|
214
|
+
|
|
215
|
+
auth = OIDCAuth(app, secret_key="aStaticSecretKey!")
|
|
216
|
+
auth.register_provider(
|
|
217
|
+
"idp",
|
|
218
|
+
token_endpoint_auth_method="client_secret_post",
|
|
219
|
+
# Replace the below values with your own
|
|
220
|
+
# NOTE: Do not hardcode your client secret!
|
|
221
|
+
client_id="<my-client-id>",
|
|
222
|
+
client_secret="<my-client-secret>",
|
|
223
|
+
server_metadata_url="<my-idp-.well-known-configuration>",
|
|
224
|
+
)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Once this is done, connecting to your app will automatically redirect to the IDP login page.
|
|
228
|
+
|
|
229
|
+
#### Multiple OIDC Providers
|
|
230
|
+
|
|
231
|
+
For multiple OIDC providers, you can use `register_provider` to add new ones after the OIDCAuth has been instantiated.
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
from dash import Dash, html
|
|
235
|
+
from dash_auth_async import OIDCAuth
|
|
236
|
+
from flask import request, redirect, url_for
|
|
237
|
+
|
|
238
|
+
app = Dash(__name__)
|
|
239
|
+
|
|
240
|
+
app.layout = html.Div([
|
|
241
|
+
html.Div("Hello world!"),
|
|
242
|
+
html.A("Logout", href="/oidc/logout"),
|
|
243
|
+
])
|
|
244
|
+
|
|
245
|
+
auth = OIDCAuth(
|
|
246
|
+
app,
|
|
247
|
+
secret_key="aStaticSecretKey!",
|
|
248
|
+
# Set the route at which the user will select the IDP they wish to login with
|
|
249
|
+
idp_selection_route="/login",
|
|
250
|
+
)
|
|
251
|
+
auth.register_provider(
|
|
252
|
+
"IDP 1",
|
|
253
|
+
token_endpoint_auth_method="client_secret_post",
|
|
254
|
+
client_id="<my-client-id>",
|
|
255
|
+
client_secret="<my-client-secret>",
|
|
256
|
+
server_metadata_url="<my-idp-.well-known-configuration>",
|
|
257
|
+
)
|
|
258
|
+
auth.register_provider(
|
|
259
|
+
"IDP 2",
|
|
260
|
+
token_endpoint_auth_method="client_secret_post",
|
|
261
|
+
client_id="<my-client-id2>",
|
|
262
|
+
client_secret="<my-client-secret2>",
|
|
263
|
+
server_metadata_url="<my-idp2-.well-known-configuration>",
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
@app.server.route("/login", methods=["GET", "POST"])
|
|
267
|
+
def login_handler():
|
|
268
|
+
if request.method == "POST":
|
|
269
|
+
idp = request.form.get("idp")
|
|
270
|
+
else:
|
|
271
|
+
idp = request.args.get("idp")
|
|
272
|
+
|
|
273
|
+
if idp is not None:
|
|
274
|
+
return redirect(url_for("oidc_login", idp=idp))
|
|
275
|
+
|
|
276
|
+
return """<div>
|
|
277
|
+
<form>
|
|
278
|
+
<div>How do you wish to sign in:</div>
|
|
279
|
+
<select name="idp">
|
|
280
|
+
<option value="IDP 1">IDP 1</option>
|
|
281
|
+
<option value="IDP 2">IDP 2</option>
|
|
282
|
+
</select>
|
|
283
|
+
<input type="submit" value="Login">
|
|
284
|
+
</form>
|
|
285
|
+
</div>"""
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
if __name__ == "__main__":
|
|
289
|
+
app.run(debug=True)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### User-group-based permissions
|
|
293
|
+
|
|
294
|
+
`dash_auth_async` provides a convenient way to secure parts of your app based on user groups.
|
|
295
|
+
|
|
296
|
+
The following utilities are defined:
|
|
297
|
+
* `list_groups`: Returns the groups of the current user, or None if the user is not authenticated.
|
|
298
|
+
* `check_groups`: Checks the current user groups against the provided list of groups.
|
|
299
|
+
Available group checks are `one_of`, `all_of` and `none_of`.
|
|
300
|
+
The function returns None if the user is not authenticated.
|
|
301
|
+
* `protected`: A function decorator that modifies the output if the user is unauthenticated
|
|
302
|
+
or missing group permission.
|
|
303
|
+
* `protected_callback`: A callback that only runs if the user is authenticated
|
|
304
|
+
and with the right group permissions.
|
|
305
|
+
|
|
306
|
+
NOTE: user info is stored in the session so make sure you define a secret_key on the Flask server
|
|
307
|
+
to use this feature.
|
|
308
|
+
|
|
309
|
+
If you wish to use this feature with BasicAuth, you will need to define the groups for individual
|
|
310
|
+
basicauth users:
|
|
311
|
+
|
|
312
|
+
```python
|
|
313
|
+
from dash_auth_async import BasicAuth
|
|
314
|
+
|
|
315
|
+
app = Dash(__name__)
|
|
316
|
+
USER_PWD = {
|
|
317
|
+
"username": "password",
|
|
318
|
+
"user2": "useSomethingMoreSecurePlease",
|
|
319
|
+
}
|
|
320
|
+
BasicAuth(
|
|
321
|
+
app,
|
|
322
|
+
USER_PWD,
|
|
323
|
+
user_groups={"user1": ["group1", "group2"], "user2": ["group2"]},
|
|
324
|
+
secret_key="Test!",
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# You can also use a function to get user groups
|
|
328
|
+
def check_user(username, password):
|
|
329
|
+
if username == "user1" and password == "password":
|
|
330
|
+
return True
|
|
331
|
+
if username == "user2" and password == "useSomethingMoreSecurePlease":
|
|
332
|
+
return True
|
|
333
|
+
return False
|
|
334
|
+
|
|
335
|
+
def get_user_groups(user):
|
|
336
|
+
if user == "user1":
|
|
337
|
+
return ["group1", "group2"]
|
|
338
|
+
elif user == "user2":
|
|
339
|
+
return ["group2"]
|
|
340
|
+
return []
|
|
341
|
+
|
|
342
|
+
BasicAuth(
|
|
343
|
+
app,
|
|
344
|
+
auth_func=check_user,
|
|
345
|
+
user_groups=get_user_groups,
|
|
346
|
+
secret_key="Test!",
|
|
347
|
+
)
|
|
348
|
+
```
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
## Dash Authorization and Login
|
|
2
|
+
|
|
3
|
+
Maintained by [joschrag](https://github.com/joschrag/). Forked from [plotly/dash-auth](https://github.com/plotly/dash-auth) with the goal to add support for the new 4.x dash backends.
|
|
4
|
+
|
|
5
|
+
License: MIT
|
|
6
|
+
|
|
7
|
+
For local testing, install [uv](https://docs.astral.sh/uv/getting-started/installation/), then install the dev dependencies and run individual tests:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
uv sync
|
|
11
|
+
uv run pytest -k ba001
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Note that Python 3.10 or greater is required.
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
### Basic Authentication
|
|
19
|
+
|
|
20
|
+
To add basic authentication, add the following to your Dash app:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from dash import Dash
|
|
24
|
+
from dash_auth_async import BasicAuth
|
|
25
|
+
|
|
26
|
+
app = Dash(__name__)
|
|
27
|
+
USER_PWD = {
|
|
28
|
+
"username": "password",
|
|
29
|
+
"user2": "useSomethingMoreSecurePlease",
|
|
30
|
+
}
|
|
31
|
+
BasicAuth(app, USER_PWD)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
One can also use an authorization python function instead of a dictionary/list of usernames and passwords:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from dash import Dash
|
|
38
|
+
from dash_auth_async import BasicAuth
|
|
39
|
+
|
|
40
|
+
def authorization_function(username, password):
|
|
41
|
+
if (username == "hello") and (password == "world"):
|
|
42
|
+
return True
|
|
43
|
+
else:
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
app = Dash(__name__)
|
|
48
|
+
BasicAuth(app, auth_func = authorization_function)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Public routes
|
|
52
|
+
|
|
53
|
+
You can whitelist routes from authentication with the `add_public_routes` utility function,
|
|
54
|
+
or by passing a `public_routes` argument to the Auth constructor.
|
|
55
|
+
The public routes should follow [Flask's route syntax](https://flask.palletsprojects.com/en/2.3.x/quickstart/#routing).
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from dash import Dash
|
|
59
|
+
from dash_auth_async import BasicAuth, add_public_routes
|
|
60
|
+
|
|
61
|
+
app = Dash(__name__)
|
|
62
|
+
USER_PWD = {
|
|
63
|
+
"username": "password",
|
|
64
|
+
"user2": "useSomethingMoreSecurePlease",
|
|
65
|
+
}
|
|
66
|
+
BasicAuth(app, USER_PWD, public_routes=["/"])
|
|
67
|
+
|
|
68
|
+
add_public_routes(app, public_routes=["/user/<user_id>/public"])
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
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.
|
|
72
|
+
Below is an example of a public route and callbacks on a multi-page Dash app using Dash's pages API:
|
|
73
|
+
|
|
74
|
+
*app.py*
|
|
75
|
+
```python
|
|
76
|
+
from dash import Dash, html, dcc, page_container
|
|
77
|
+
from dash_auth_async import BasicAuth
|
|
78
|
+
|
|
79
|
+
app = Dash(__name__, use_pages=True, suppress_callback_exceptions=True)
|
|
80
|
+
USER_PWD = {
|
|
81
|
+
"username": "password",
|
|
82
|
+
"user2": "useSomethingMoreSecurePlease",
|
|
83
|
+
}
|
|
84
|
+
BasicAuth(app, USER_PWD, public_routes=["/", "/user/<user_id>/public"])
|
|
85
|
+
|
|
86
|
+
app.layout = html.Div(
|
|
87
|
+
[
|
|
88
|
+
html.Div(
|
|
89
|
+
[
|
|
90
|
+
dcc.Link("Home", href="/"),
|
|
91
|
+
dcc.Link("John Doe", href="/user/john_doe/public"),
|
|
92
|
+
],
|
|
93
|
+
style={"display": "flex", "gap": "1rem", "background": "lightgray", "padding": "0.5rem 1rem"},
|
|
94
|
+
),
|
|
95
|
+
page_container,
|
|
96
|
+
],
|
|
97
|
+
style={"display": "flex", "flexDirection": "column"},
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
app.run(debug=True)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
*pages/home.py*
|
|
106
|
+
```python
|
|
107
|
+
from dash import Input, Output, html, register_page
|
|
108
|
+
from dash_auth_async import public_callback
|
|
109
|
+
|
|
110
|
+
register_page(__name__, "/")
|
|
111
|
+
|
|
112
|
+
layout = [
|
|
113
|
+
html.H1("Home Page"),
|
|
114
|
+
html.Button("Click me", id="home-button"),
|
|
115
|
+
html.Div(id="home-contents"),
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
# Note the use of public callback here rather than the default Dash callback
|
|
119
|
+
@public_callback(
|
|
120
|
+
Output("home-contents", "children"),
|
|
121
|
+
Input("home-button", "n_clicks"),
|
|
122
|
+
)
|
|
123
|
+
def home(n_clicks):
|
|
124
|
+
if not n_clicks:
|
|
125
|
+
return "You haven't clicked the button."
|
|
126
|
+
return "You clicked the button {} times".format(n_clicks)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
*pages/public_user.py*
|
|
131
|
+
```python
|
|
132
|
+
from dash import html, dcc, register_page
|
|
133
|
+
|
|
134
|
+
register_page(__name__, path_template="/user/<user_id>/public")
|
|
135
|
+
|
|
136
|
+
def layout(user_id: str):
|
|
137
|
+
return [
|
|
138
|
+
html.H1(f"User {user_id} (public)"),
|
|
139
|
+
dcc.Link("Authenticated user content", href=f"/user/{user_id}/private"),
|
|
140
|
+
]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
*pages/private_user.py*
|
|
145
|
+
```python
|
|
146
|
+
from dash import html, register_page
|
|
147
|
+
|
|
148
|
+
register_page(__name__, path_template="/user/<user_id>/private")
|
|
149
|
+
|
|
150
|
+
def layout(user_id: str):
|
|
151
|
+
return [
|
|
152
|
+
html.H1(f"User {user_id} (authenticated only)"),
|
|
153
|
+
html.Div("Members-only information"),
|
|
154
|
+
]
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### OIDC Authentication
|
|
158
|
+
|
|
159
|
+
To add authentication with OpenID Connect, you will first need to set up an OpenID Connect provider (IDP).
|
|
160
|
+
This typically requires creating
|
|
161
|
+
* An application in your IDP
|
|
162
|
+
* Defining the redirect URI for your application, for testing locally you can use http://localhost:8050/oidc/callback
|
|
163
|
+
* A client ID and secret for the application
|
|
164
|
+
|
|
165
|
+
Once you have set up your IDP, you can add it to your Dash app as follows:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from dash import Dash
|
|
169
|
+
from dash_auth_async import OIDCAuth
|
|
170
|
+
|
|
171
|
+
app = Dash(__name__)
|
|
172
|
+
|
|
173
|
+
auth = OIDCAuth(app, secret_key="aStaticSecretKey!")
|
|
174
|
+
auth.register_provider(
|
|
175
|
+
"idp",
|
|
176
|
+
token_endpoint_auth_method="client_secret_post",
|
|
177
|
+
# Replace the below values with your own
|
|
178
|
+
# NOTE: Do not hardcode your client secret!
|
|
179
|
+
client_id="<my-client-id>",
|
|
180
|
+
client_secret="<my-client-secret>",
|
|
181
|
+
server_metadata_url="<my-idp-.well-known-configuration>",
|
|
182
|
+
)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Once this is done, connecting to your app will automatically redirect to the IDP login page.
|
|
186
|
+
|
|
187
|
+
#### Multiple OIDC Providers
|
|
188
|
+
|
|
189
|
+
For multiple OIDC providers, you can use `register_provider` to add new ones after the OIDCAuth has been instantiated.
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from dash import Dash, html
|
|
193
|
+
from dash_auth_async import OIDCAuth
|
|
194
|
+
from flask import request, redirect, url_for
|
|
195
|
+
|
|
196
|
+
app = Dash(__name__)
|
|
197
|
+
|
|
198
|
+
app.layout = html.Div([
|
|
199
|
+
html.Div("Hello world!"),
|
|
200
|
+
html.A("Logout", href="/oidc/logout"),
|
|
201
|
+
])
|
|
202
|
+
|
|
203
|
+
auth = OIDCAuth(
|
|
204
|
+
app,
|
|
205
|
+
secret_key="aStaticSecretKey!",
|
|
206
|
+
# Set the route at which the user will select the IDP they wish to login with
|
|
207
|
+
idp_selection_route="/login",
|
|
208
|
+
)
|
|
209
|
+
auth.register_provider(
|
|
210
|
+
"IDP 1",
|
|
211
|
+
token_endpoint_auth_method="client_secret_post",
|
|
212
|
+
client_id="<my-client-id>",
|
|
213
|
+
client_secret="<my-client-secret>",
|
|
214
|
+
server_metadata_url="<my-idp-.well-known-configuration>",
|
|
215
|
+
)
|
|
216
|
+
auth.register_provider(
|
|
217
|
+
"IDP 2",
|
|
218
|
+
token_endpoint_auth_method="client_secret_post",
|
|
219
|
+
client_id="<my-client-id2>",
|
|
220
|
+
client_secret="<my-client-secret2>",
|
|
221
|
+
server_metadata_url="<my-idp2-.well-known-configuration>",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
@app.server.route("/login", methods=["GET", "POST"])
|
|
225
|
+
def login_handler():
|
|
226
|
+
if request.method == "POST":
|
|
227
|
+
idp = request.form.get("idp")
|
|
228
|
+
else:
|
|
229
|
+
idp = request.args.get("idp")
|
|
230
|
+
|
|
231
|
+
if idp is not None:
|
|
232
|
+
return redirect(url_for("oidc_login", idp=idp))
|
|
233
|
+
|
|
234
|
+
return """<div>
|
|
235
|
+
<form>
|
|
236
|
+
<div>How do you wish to sign in:</div>
|
|
237
|
+
<select name="idp">
|
|
238
|
+
<option value="IDP 1">IDP 1</option>
|
|
239
|
+
<option value="IDP 2">IDP 2</option>
|
|
240
|
+
</select>
|
|
241
|
+
<input type="submit" value="Login">
|
|
242
|
+
</form>
|
|
243
|
+
</div>"""
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
if __name__ == "__main__":
|
|
247
|
+
app.run(debug=True)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### User-group-based permissions
|
|
251
|
+
|
|
252
|
+
`dash_auth_async` provides a convenient way to secure parts of your app based on user groups.
|
|
253
|
+
|
|
254
|
+
The following utilities are defined:
|
|
255
|
+
* `list_groups`: Returns the groups of the current user, or None if the user is not authenticated.
|
|
256
|
+
* `check_groups`: Checks the current user groups against the provided list of groups.
|
|
257
|
+
Available group checks are `one_of`, `all_of` and `none_of`.
|
|
258
|
+
The function returns None if the user is not authenticated.
|
|
259
|
+
* `protected`: A function decorator that modifies the output if the user is unauthenticated
|
|
260
|
+
or missing group permission.
|
|
261
|
+
* `protected_callback`: A callback that only runs if the user is authenticated
|
|
262
|
+
and with the right group permissions.
|
|
263
|
+
|
|
264
|
+
NOTE: user info is stored in the session so make sure you define a secret_key on the Flask server
|
|
265
|
+
to use this feature.
|
|
266
|
+
|
|
267
|
+
If you wish to use this feature with BasicAuth, you will need to define the groups for individual
|
|
268
|
+
basicauth users:
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
from dash_auth_async import BasicAuth
|
|
272
|
+
|
|
273
|
+
app = Dash(__name__)
|
|
274
|
+
USER_PWD = {
|
|
275
|
+
"username": "password",
|
|
276
|
+
"user2": "useSomethingMoreSecurePlease",
|
|
277
|
+
}
|
|
278
|
+
BasicAuth(
|
|
279
|
+
app,
|
|
280
|
+
USER_PWD,
|
|
281
|
+
user_groups={"user1": ["group1", "group2"], "user2": ["group2"]},
|
|
282
|
+
secret_key="Test!",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# You can also use a function to get user groups
|
|
286
|
+
def check_user(username, password):
|
|
287
|
+
if username == "user1" and password == "password":
|
|
288
|
+
return True
|
|
289
|
+
if username == "user2" and password == "useSomethingMoreSecurePlease":
|
|
290
|
+
return True
|
|
291
|
+
return False
|
|
292
|
+
|
|
293
|
+
def get_user_groups(user):
|
|
294
|
+
if user == "user1":
|
|
295
|
+
return ["group1", "group2"]
|
|
296
|
+
elif user == "user2":
|
|
297
|
+
return ["group2"]
|
|
298
|
+
return []
|
|
299
|
+
|
|
300
|
+
BasicAuth(
|
|
301
|
+
app,
|
|
302
|
+
auth_func=check_user,
|
|
303
|
+
user_groups=get_user_groups,
|
|
304
|
+
secret_key="Test!",
|
|
305
|
+
)
|
|
306
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .public_routes import add_public_routes, public_callback
|
|
2
|
+
from .basic_auth import BasicAuth
|
|
3
|
+
from .group_protection import list_groups, check_groups, protected, protected_callback
|
|
4
|
+
|
|
5
|
+
# oidc auth requires authlib, install with `pip install dash-auth[oidc]`
|
|
6
|
+
try:
|
|
7
|
+
from .oidc_auth import OIDCAuth, get_oauth
|
|
8
|
+
except ModuleNotFoundError:
|
|
9
|
+
pass
|
|
10
|
+
from .version import __version__
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"add_public_routes",
|
|
15
|
+
"check_groups",
|
|
16
|
+
"list_groups",
|
|
17
|
+
"get_oauth",
|
|
18
|
+
"protected",
|
|
19
|
+
"protected_callback",
|
|
20
|
+
"public_callback",
|
|
21
|
+
"BasicAuth",
|
|
22
|
+
"OIDCAuth",
|
|
23
|
+
"__version__",
|
|
24
|
+
]
|