dash-auth icon indicating copy to clipboard operation
dash-auth copied to clipboard

auth-flow adjustments

Open BSd3v opened this issue 11 months ago • 4 comments

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 the restricted_users_lookup dict passed as kwargs
  • this will also use a user_session_key to know whether this is matched in the returning list of restricted_users
  • protect_layouts to protect all layouts in the app
  • auth_protect_layouts tells the app to invoke the protect_layouts with the public_routes passed to not protect the layouts of public routes.
  • auth_protect_layouts_kwargs is the same are the additional kwargs passed to the function
  • page_container as the id 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 the group_lookup dict passed as kwargs

BSd3v avatar Mar 21 '24 18:03 BSd3v

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)

BSd3v avatar Mar 21 '24 20:03 BSd3v

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.

RenaudLN avatar Mar 21 '24 21:03 RenaudLN

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.

BSd3v avatar Mar 21 '24 21:03 BSd3v

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

BSd3v avatar Apr 09 '24 04:04 BSd3v