Multi-page authentication using layouts as functions

Been playing around with making authentication smooth on multi-page apps.

Created dash-auth-flow as an example of a smooth authentication flow, but it still had the problem of loading some content before triggering a callback that then checked authentication; each additional function had to then verify authentication. This could easy result in information getting exposed.

So I tried making layouts into conditional functions: if the user is authenticated, return the sensitive layout, otherwise return something else e.g. a dcc.Location(pathname='/login',refresh=True) which takes the user to a login page. This works!

You can show part of the app and not another, e.g. homepage but not user profile. You can even define all the callbacks you need as long as the part of the page you show doesn’t trigger any callbacks that reference non-existent component IDs.

I will use this in flask_login e.g. if current_user.is_authenticated... .

MWE:

test/
    pages/
        profile.py
    app.py
    server.py

server.py

import dash
app = dash.Dash(__name__)
app.config.suppress_callback_exceptions = True

app.py

import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State
from pages import profile
from server import app

app.layout = html.Div([
    dcc.Location(id='base-url',refresh=True,pathname='/'),
    html.Button('click this',id='button',n_clicks=0),
    # links to pages
    html.Div([dcc.Link('profile',href='/profile'),html.Br(),dcc.Link('base',href='/'),html.Br()]),
    html.Div(id='page-content')
])

# show how many times the button has been clicked
@app.callback(
    Output('button','children'),
    [Input('button','n_clicks')])
def show_clicks(n):
    return 'click this button, clicked {} times'.format(str(n))

# router function
@app.callback(
    Output('page-content','children'),
    [Input('base-url','pathname')],
    [State('button','n_clicks')])
def router(pathname,n):
    if pathname=='/':
        return 'Base content'
    elif pathname=='/profile':
        return profile.profile_layout(n)
    return '404'

if __name__=='__main__':
    app.run_server(debug=True)

pages/profile.py

import dash_html_components as html
import dash_core_components as dcc
from dash.dependencies import Output,Input,State
from server import app

# example of a layout function - usually imported from other 
# only show the profile layout if the button has been clicked more than two times
# otherwise, send back to /base
def profile_layout(n):
    if n>2:
        return [dcc.Location(id='profile-url',refresh=True,pathname='/profile'),
                html.Div(id='profile-picture')]
    else:
        return [dcc.Location(id='profile-url',refresh=True,pathname='/')]

# profile callback
@app.callback(
    Output('profile-picture','children'),
    [Input('profile-picture','style')])
def return_profile_picture(s):
    return ['that worked!',html.Img(src='https://picsum.photos/500/500')]

Tried clicking ‘Profile’ without any button clicks:

image

Clicked “Profile” with more button clicks:

Has anyone done anything like this?

This is how many popular web frameworks work e.g. Django. The solution is to decorate your view functions (or layout) with something like @login_required https://docs.djangoproject.com/en/2.2/topics/auth/default/#the-login-required-decorator

This seems like it could be adapted for this use case.

2 Likes

I’ve used @login_required in other Flask apps but it never quite hit me that I could use it in the context of Dash apps…looked into how to make a custom decorator. This is great! Thanks for the tip!

Came up with this to do the same thing without the if else every time:

def layout_auth(mode,children):
    '''
    if mode is 'auth':
        if user is not authenticated, returns children instead of the output of the function

    if mode is 'nonauth':
        if user is authenticated, returns children instead of the output of the function
    '''
    def this_decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            if mode=='auth':
                if current_user.is_authenticated:
                    return f(*args, **kwargs)
                return children
            else:# mode=='nonauth':
                if not current_user.is_authenticated:
                    return f(*args, **kwargs)
                return children
        return decorated_function
    return this_decorator

Again, big thanks for the tip! I’ll definitely be using this going forward.

2 Likes