How Protect Dash Routes With Login After Dash 1.0 Release

So where’s the Dash 1.0 release thread? The part about making dash work like other flask extensions excites me more than anything. Anyway,…

Before Dash 1.0 release, I had a function like below that I would call after creating dash app and setting a flask app to it as the server.

def protect_views(app):
    for view_func in app.server.view_functions:
        if view_func.startswith(app.url_base_pathname):
            app.server.view_functions[view_func] = login_required(app.server.view_functions[view_func])

    return app

With the Dash 1.0 update, there’s a nice and consistent way to declare a dash app without a server and then using the the init_app(app) function to pass a standard flask application to it. My question is, if I’m also using flask-login, how do I protect the routes of the dash app after calling the init_app to give dash a server? My setup with the factory pattern with Blueprints is shown below.

I have an application_folder which will contain my flask app and its routes, etc. application_folder has a folder called dash_application. dash_application folder contains dash_example.py as below:

##########################
import glob
from pathlib import Path
from dash import Dash
import dash_table
import dash_html_components as html
import dash_core_components as dcc
import pandas as pd
from .layout import html_layout
from flask_login import login_required


def dash_application():
    # Create a Dash app
    dash_app = Dash(__name__, 
                    server=False, # Don't give dash a server just yet.
                    url_base_pathname='/dashapp/')
    

    # Create Dash Graph
    dash_app.layout = html.Div([
    html.H1('Dash application'),
    dcc.Graph(
        id='basic-graph',
        figure={
            'data':[
                {
                    'x': [0, 1],
                    'y': [0, 1],
                    'type': 'line'
                }
            ],
            'layout': {
                'title': 'Basic Graph'
                }
                }
            )
        ])

    return dash_app
##########################

The application_folder has my init file as:

#############################################
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, login_required
from .dash_application import dash_example


## Globally accessible libraries
db = SQLAlchemy()
login_manager = LoginManager()
login_manager.login_view = 'auth_bp.login'
dash = dash_example.dash_application()



def create_app():
    # Initialize the core application.
    app = Flask(__name__, instance_relative_config=False)
    app.config.from_object('config.Config')

    # Initialize Plugins
    db.init_app(app)
    login_manager.init_app(app)
    
    # This is where a dash app attaches to a server
    dash.init_app(app=app)

    with app.app_context():
        # Set global variables


        # Include our Routes
        from . import routes
        from . import auth


        # Register Blueprints
        app.register_blueprint(auth.auth_bp)
        app.register_blueprint(routes.main_bp)

        db.create_all()

        return app
############################################

application_folder also has routes.py

###############################
import os
from flask import Blueprint, render_template, redirect, url_for
from flask_login import current_user, login_required


main_bp = Blueprint('main_bp', __name__,
                    template_folder='templates',
                    static_folder='static')


@main_bp.route('/')
@login_required
def home():
    return render_template("index.html")


@main_bp.route('/page2')
@login_required
def home2():
    return "This is page 2"
####################################

application_folder also has auth.py

###################################
import os
from flask import Blueprint, render_template, redirect, url_for, request
from flask_login import current_user, login_required, login_user, logout_user
from werkzeug.urls import url_parse


from .models import User
from .forms import LoginForm
from . import db


auth_bp = Blueprint('auth_bp', __name__,
                    template_folder='templates',
                    static_folder='static')


@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('main_bp.home'))
    
    form = LDAPLoginForm()
    if form.validate_on_submit():
        login_user(user=form.user)
        next_page = request.args.get('next')
        if not next_page or url_parse(next_page).netloc != '':
            next_page = url_for('main_bp.home')
        return redirect(next_page)

    return render_template('login.html', title='Sign In', form=form)

@auth_bp.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for("auth_bp.login"))
##########################################

The route for the dash app (/dashapp/) as specified in the dash_example.py file is currently not protected from unauthorized user sessions. I need to protect that route like I have done for ‘/’ and ‘/page2’.

Before the dash 1.0 update, I could call the protect_views function and pass the dash application to it. Since it had a server at that point, the function would protect that route. Now, with the update, the dash app doesn’t get a server until the init_app is called on it. This happens in my init file as part of the factory pattern. After dash gets a flask server, I do not see the url_base_pathname attribute of the dash instance anymore. So that function doesn’t work anymore.

I simply want to be able to protect the view functions on the dash app itself. Any help would be appreciated.

2 Likes

I get this error:

AttributeError: 'Dash' object has no attribute 'url_base_pathname'

I do not understand how I was able to set url_base_pathname when declaring the dash app but then it does not have this attribute. Did something change in dash 1.0 with this?

Yes - Dash 1.0 moved nearly all the constructor kwargs into app.config rather than direct attributes of the app.

We also restrict some of them to be read-only - including url_base_pathname, because changing it after construction wouldn’t have the same effect as setting a different value in the constructor. Might be able to relax that restriction if needed, for now it was just reflecting the existing situation.

1 Like

Thanks so much! Next time, I have to read the docs before asking questions.

Hi @alexcjohnson, @okyere,

I have a very similar application to @okyere and I’m trying to protect the dashboard built in dash. From what I can understand, the protect_views function should do the trick. I’m new to dash, flask and python in general and am not entirely sure where in the code exactly should the function be called.

Thank you!!!

@dalo, welcome to python, flask, and dash!

You will call protect_views function in the init file after the lines of code that register the blueprints but before the line of code that returns the app. So it could look like this:

app.register_blueprint(auth.auth_bp)
app.register_blueprint(routes.main_bp)
dash_example.protect_views(dash) #This is where you call the protect_views function
db.create_all()
return app

Good luck with you flask and dash journey. If you need further help, ask.

2 Likes

Thank you very much, @okyere! I stopped getting the errors I was getting previously but it still doesn’t seem to be working right. I’m getting KeyError: ‘url_base_pathname’.

I maybe missing something else would you mind going quickly through my code? It may be something ‘stupid’ that I left in the code but don’t see. It may be more obvious to someone who’s done this before.

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, login_required

def protect_views(app):
    for view_func in app.view_functions:
        if view_func.startswith(app.config['url_base_pathname']):
            app.view_functions[view_func] = login_required(app.view_functions[view_func])

db = SQLAlchemy()

def create_app():
    app = Flask(__name__)

    app.config['SECRET_KEY'] = 'my-very-secret-code'
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite'

    db.init_app(app)

    login_manager = LoginManager()
    login_manager.login_view = 'auth.login'
    login_manager.init_app(app)

    from .models import user

    @login_manager.user_loader
    def load_user(user_id):
        return user.query.get(int(user_id))

    from .auth import auth as auth_blueprint
    app.register_blueprint(auth_blueprint)

    from .main import main as main_blueprint
    app.register_blueprint(main_blueprint)

    from .dash_app.dashboard import init_dash_app
    app = init_dash_app(app)

    app = protect_views(app)
        
    return app

I tried to consult a number of other forums but can’t identify the error in the code so thank you very much for your help and time!

@dalo
Can you share the file where the dash app was created? Here’s my updated dash_example.py from the original question I asked above:

"""Create a Dash app within a Flask app."""
import glob
from pathlib import Path
from dash import Dash
import dash_table
import dash_html_components as html
import dash_core_components as dcc
import pandas as pd
from .layout import html_layout
from flask_login import login_required


def dash_application():
    """Create a Dash app."""
    dash_app = Dash(__name__, 
                    server=False, 
                    url_base_pathname='/dashapp/')


    # Create Dash Graph
    dash_app.layout = html.Div([
    html.H1('Dash application'),
    dcc.Graph(
        id='basic-graph',
        figure={
            'data':[
                {
                    'x': [0, 1],
                    'y': [0, 1],
                    'type': 'line'
                }
            ],
            'layout': {
                'title': 'Basic Graph'
                }
                }
            )
        ])

    return dash_app

# Function to protect dashboard views from unauthenticated users
def protect_views(app):
    for view_func in app.server.view_functions:
        if view_func.startswith(app.config["url_base_pathname"]):
            app.server.view_functions[view_func] = login_required(app.server.view_functions[view_func])
    return app

You’ll note that I defined the protect_views() functions in this file. Also note how the dash_app was initiated. If this doesn’t help, please share the rest of your code especially where you instantiate the dash app.

2 Likes

@okyere
I tried and no luck, unfortunately. See the two files below. All your help is really appreciated, thank you!

When I hash the line including ‘app = protect_views(dash)’, the code seems to be okay as I can access the url and see the dash app. Once I unhash the line, I get a new error message: ‘Flask’ object has not attribute ‘server’.

"""__init__.py"""
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager

db = SQLAlchemy()

def create_app():
    app = Flask(__name__)

    app.config['SECRET_KEY'] = 'my-very-secret-code'
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite'

    db.init_app(app)

    login_manager = LoginManager()
    login_manager.login_view = 'auth.login'
    login_manager.init_app(app)

    from .models import user

    @login_manager.user_loader
    def load_user(user_id):
        return user.query.get(int(user_id))

    from .auth import auth as auth_blueprint
    app.register_blueprint(auth_blueprint)

    from .main import main as main_blueprint
    app.register_blueprint(main_blueprint)

    from .dash_app.dashboard import init_dash_app, protect_views
    dash = init_dash_app(app)
    app = protect_views(dash)
        
    return app


""".dash_app/dashboard.py"""
import dash
import dash_html_components as html
from flask_login import login_required

def init_dash_app(server):
    dash_otc = dash.Dash(
        server = server,
        url_base_pathname = '/database/'
        )

    dash_otc.layout = html.Div([
        html.H1('Am I finally password protected?')
        ])

    return dash_otc.server

def protect_views(app):
    for view_func in app.server.view_functions:
        if view_func.startswith(app.config['url_base_pathname']):
            app.server.view_functions[view_func] = login_required(app.server.view_functions[view_func])
    return app

When I define the init_dash_app() function exactly like you (see below), I get a an error ‘NoneType’ object has no attribute ‘view_functions’.

import dash
import dash_html_components as html
from flask_login import login_required

def init_dash_app():
    dash_otc = dash.Dash(
        __name__,
        server = False,
        url_base_pathname = '/database/'
        )

    dash_otc.layout = html.Div([
        html.H1('Am I finally password protected?')
        ])

    return dash_otc

def protect_views(app):
    for view_func in app.server.view_functions:
        if view_func.startswith(app.config['url_base_pathname']):
            app.server.view_functions[view_func] = login_required(app.server.view_functions[view_func])
    return app

Again, thank you very much for all the help!

No worries @okyere! Just figured it out! I was wrongly initiating the dash app!

Thank you very much!

Take care and good luck with python, dash and data analysis!

@dalo I’m glad to know you got a resolution to this issue. Good luck.

@adamschroeder has a great youtube channel solely focused on dash. Check it out sometime.

3 Likes

I’ll check his channel. Thank you again, @okyere!

Hi @okyere @alexcjohnson and @dalo, just want to follow up with this thread as I am trying to embed Dash app with Flask in similar way. My issue is that login_manager.current_user always return None in the Dash app, but can be accessed successfully from other routes. This is the init.py to create flask server and register Dash view and other blueprint:

from dash import Dash
from flask_login import LoginManager, login_required, login_user, current_user
from .routes import main_bp

def construct_dash_app(app, url_base_pathname):
    return Dash(__name__,
                server=app,
                url_base_pathname=url_base_pathname,
                meta_tags=[meta_viewport],
                suppress_callback_exceptions = True)

def register_dash_callbacks(dash_app, title, layout, register_func):
    dash_app.title = title
    dash_app.layout = layout
    register_func(dash_app)

def register_dashapps(app):

    # Import dash app
    from python.fmapp.layout import layout as fm_layout
    from python.fmapp.callbacks import register_callbacks as fm_register_callbacks

    fmapp = construct_dash_app(app, '/fmapp/')

    with app.app_context():
        from python import routes

        routes.login_manager.init_app(app)
        register_dash_callbacks(fmapp, 'fmapp', fm_layout, fm_register_callbacks)
        app.register_blueprint(routes.main_bp)

        for view_name in app.view_functions.keys():
            if view_name.startswith(("/fmapp/")):
                app.view_functions[view_name] = login_required(app.view_functions[view_name])

def create_app():
    LOG.info("Updating flask config.")
    app.config.update(
        SECRET_KEY = os.urandom(16),
        # LOGIN_DISABLED = os.environ.get("SysLevel", "") == "dev"
    )

    LOG.info("Registering Dash apps.")
    register_dash.register_dashapps(app)

    LOG.info("Apps successfully created.")
    return app

In routes.py I have another blueprint and login_manager:

from flask import Blueprint, render_template, escape, url_for, redirect
from flask_login import LoginManager, login_required, login_user, UserMixin, current_user

main_bp = Blueprint('main_bp', __name__)
login_manager = LoginManager()
@main_bp.route('/', methods=['GET', 'POST'])
@login_required
def index():
    LOG.debug(DEBUG_ROUTE_MSG.format("/"))
    return render_template("index.html", title='Home Page')

@main_bp.route('/access_denied')
@login_manager.unauthorized_handler
def unauthorized():
    return render_template("access_denied.html", title="Unauthorized Access")

@login_manager.request_loader
def load_user(request):
    auth_status = validate_auth(request)
    if auth_status[0]:
        LOG.debug("User has been authorized")
        token = auth_status[1]
        user = User(token.get("sub"), token.get("given_name"), token.get("family_name"), token.get("email"), token.get("groups"))
        return user
    else:
        user = User('None', 'ABC', 'X', 'abc_x@vanguard.com', 'user')
        LOG.debug("Not auth user!!!!!!!!!!!")
        return user

class User(UserMixin):

    def __init__(self, user_id, first_name, last_name, email, groups):
        self.id = user_id
        self.email = email
        self.first_name = first_name
        self.last_name = last_name
        self.full_name = "{} {}".format(first_name, last_name)
        self.group= groups

I skipped auth function which could decode user information. All routes except dash app could return correct value for current_user. This is import current_user in dash callbacks.py

from flask_login import current_user

print('User!!!!!!!!!!!!!!!!!!!!!!!', current_user)

Do you know why Dash can’t get current_user even I applied login_request for dash view? Thank you very much for all the help!

Hi @jing0703,

in my auht.py file I save the current user into session[‘name’] like below:

@auth.route('/login', methods=['POST'])
def login_post():
    email = request.form.get('email')
    password = request.form.get('password')
    remember = True if request.form.get('remember') else False

    exists = user.query.filter_by(email=email).first()

    if not exists or not check_password_hash(exists.password, password):
        flash('Entered credentials seem wrong. Please, try again.')
        return redirect(url_for('auth.login'))

    login_user(exists, remember = remember)
    session['name'] = current_user.name
    return redirect(url_for('main.profile'))

In the dash app, I also

from flask import session

and then I followed the solution from here: https://stackoverflow.com/questions/58379772/passing-current-user-from-flask-login-to-plotly-dash-app where you can return the current_user’s name in a callback. The code below is taken from the stackoverflow forum. You’ll then have to build the dash app in the return of this callback. But don’t worry, all other callbacks should then work perfectly fine. They do in my case.

app1 = dash.Dash(__name__, server = application, routes_pathname_prefix = '/app1/', assets_folder = 'static', assets_url_path = '/static')
app1.scripts.config.serve_locally = True
app1.css.config.serve_locally = True

app1.layout = html.Div(
    children = [
        html.Div(id='div2'),
        html.Div(id='div3', children = 'xxxx'),
    ],
)

@app1.callback(
    Output('div2', 'children'),
    [Input('div3', 'children')])
def update_intervalCurrentTime(children):
    return session.get('username', None)

Hope this helps!

1 Like

Hi @dalo, thank you so much for help with it! It worked for me too! In another example (https://github.com/jimmybow/Flask_template_auth_with_Dash/blob/master/Dashboard/Dash_fun.py),
The author registered dash app in flask server with ‘Blueprint’ not ‘View’. In this way, dash app can access ‘current_user’ as global param. Not sure what’s the difference between Blueprint view and the way we set up dash app. So happy that I don’t need to restructure my app, thanks again for all the help!!

1 Like

Thanks @jing0703,

this second workaround will be useful for me too! Glad you managed to make it work

1 Like