dash-auth
dash-auth copied to clipboard
auth-flow adjustments
added:
- user groups to the OIDC flow
- added the ability to pass your own login_user_callback to be able to customize the flow
- restritected users as a list or a function
- when a function is passed, it will execute during the function of
check_groups
with therestricted_users_lookup
dict passed as kwargs - this will also use a
user_session_key
to know whether this is matched in the returning list ofrestricted_users
-
protect_layouts
to protect all layouts in the app -
auth_protect_layouts
tells the app to invoke theprotect_layouts
with thepublic_routes
passed to not protect the layouts of public routes. -
auth_protect_layouts_kwargs
is the same are the additionalkwargs
passed to the function -
page_container
as theid
of your container element for a page container, it will only check the route if it is an output.
changed:
- if a
idp_selection_route
is available, it will cater this even if there is only one provider, this allows for legacy logins - groups is now a list of strings or a function
- when a function is passed, it will execute during the function of
check_groups
with thegroup_lookup
dict passed as kwargs
here is a app that I tested this with:
from dash import Dash, html, Input, Output
from dash_auth import OIDCAuth, protected, protected_callback
from flask import request, redirect, url_for, session
from flask_login import current_user, LoginManager, login_user, logout_user, UserMixin
app = Dash(__name__)
login_manager = LoginManager()
login_manager.init_app(app.server)
class User(UserMixin):
pass
@login_manager.user_loader
def user_loader(username):
user = User()
user.id = username
return user
def all_login_method(user_info, idp=None):
if idp:
session["user"] = user_info
session["idp"] = idp
session['user']['groups'] = ['this', 'is', 'a', 'testing']
else:
user = User()
user.id = user_info.get('user')
login_user(user)
session['user'] = {}
session['user']['groups'] = ['nah']
session['user']['email'] = user_info.get('user')
return redirect(app.config.get("url_base_pathname") or "/")
def get_groups(**kwargs):
if kwargs.get('testing'):
return ['nah']
return ['testing', 't2', 't3']
restricted_users = ['test', 'testing']
def restricted_user(**kwargs):
return restricted_users
def layout():
if request:
if current_user:
try:
return html.Div([
html.Div(f"Hello {current_user.id}!"),
html.Button(id='change_users', children='change restrictions'),
html.Button(id='test', children='you cant use me'),
html.A("Logout", href="/oidc/logout"),
])
except:
pass
if 'user' in session:
return html.Div([
html.Div(f"""Hello {session['user'].get('email')}!
You have access to these groups: {session['user'].get('groups')}"""),
html.Button(id='change_users', children='change restrictions'),
html.Button(id='test', children='you cant use me'),
html.A("Logout", href="/oidc/logout"),
])
return html.Div([
html.Div("Hello world!"),
html.Button(id='change_users', children='change restrictions'),
html.Button(id='test', children='you cant use me'),
html.A("Logout", href="/oidc/logout"),
])
app.layout = layout
@protected_callback(
Output('test', 'children'),
Input('test', 'n_clicks'),
groups=get_groups,
prevent_initial_call=True,
group_lookup={'testing': 'rawr'},
restricted_users=restricted_user
)
def testing(n):
return 'I was clicked'
@protected_callback(
Output('change_users', 'children'),
Input('change_users', 'n_clicks'),
prevent_initial_call=True
)
def alter_restrictions(n):
global restricted_users
restricted_users = ['rawr', 'hahaha']
return 'Users changed'
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",
user_groups={'useremail': ['rawr']},
login_user_callback=all_login_method
)
auth.register_provider(
"IDP 1",
token_endpoint_auth_method="client_secret_post",
client_id="",
client_secret="",
server_metadata_url=f""
)
@app.server.route("/login", methods=["GET", "POST"])
def login_handler():
if request.method == 'POST':
form_data = request.form
else:
form_data = request.args
if form_data.get('user') and form_data.get('password'):
return all_login_method(form_data)
if form_data.get('IDP 1'):
return redirect(url_for("oidc_login", idp='IDP 1'))
return """<div>
<form method="POST">
<div>How do you wish to sign in:</div>
<button type="submit" name="IDP 1" value="true">Microsoft</button>
<div><input name="user"/>
<input name="password"/></div>
<input type="submit" value="Login">
</form>
</div>"""
if __name__ == "__main__":
app.run_server(debug=True)
With OIDC the groups should be defined in the identity provider and passed via custom claims on sign-in.
The process to do this depends on the IDP, it might also require to have an additional scope in OIDCAuth client kwargs.
The OIDC may not have associated groups that you want your app to control. Thus allowing for the two to be independent, especially in a B2B solution.
For example, business a might want to have individual control over what their users can and cannot do inside of the app, operational managers (admins) would be able to adjust these on an individual bases per user without adjusting their overarching groups inside of the organization that the users belong to.
This does not replace your parsing of strings or OIDC group keys, but extends its ability.
In fact, you could even append to the stuff that the org has in theirs as well, since you have access to the session which is housing the info from the idp.
Wouldnt these be protected by the callbacks themselves and not need to be protected by a pathname?
https://github.com/plotly/dash-auth/pull/148#discussion_r1556724683