Basic Auth with Multi-Page apps... incompatible?

I currently use Basic Auth in a simple dashboard. However, if I wanted to convert this dashboard into a Multi-Page app, I don’t think Basic Auth is compatible. It will work, yes, but 403 errors will occur quite often for the user.

Basically, if I was to send someone a link to any specific pages other than the root, the app will return a 403 error.
127.0.0.1:8050 —> Brings up the login menu
127.0.0.1:8050/an_interesting_page ----> 403 error (the url prevents the app from bringing up the login menu)

So I would only ever be able to link people to the root page, which is not a great user experience. Is there a workaround for this, or perhaps a better alternative to Basic Auth? I’ve seen similar questions asked before, but they all have no replies. Example 1. Example 2.

Kinda decided against using the default auth system of Plotly and built my own, to be fair I never really gave plotly auth a try lol I do use multi pages and have achieved private authentication from a Django backend. The way I was able to do this was to create a Django project and setup an API (using ninja), DRF or Graphql or any other API provider should work but the code going to look a little different.

Then I created a class and a route to the URL via Django and the views.py like so:

from ninja import NinjaAPI
from ninja.security import HttpBearer
from ninja.security import HttpBasicAuth
from django.contrib.auth.models import User
from django.contrib.auth import authenticate as auth_authenticate

api = NinjaAPI()

class BasicAuth(HttpBasicAuth):
    def authenticate(self, request, username, password):
        # Request all Users on Application
        users = User.objects.all()
        # check username vs user in database
        user_in_users = users.values_list('username', flat=True)
        # print('user_in_users', list(user_in_users))

        emails = users.values_list('email', flat=True)
        # print('emails', list(emails))

        if username in list(user_in_users):
            user = auth_authenticate(username=username, password=password)
            if user is not None:
                # the credentials are valid
                return username
            else:
                # the credentials are invalid
                return None
        elif username in list(emails):
            print('Wasn\'t able to find Username, trying email')
            if username in emails:
                print('Email in database')
                u = User.objects.get(email=username)

                user = auth_authenticate(username=u, password=password)
                if user is not None:
                    # the credentials are valid
                    return username
                else:
                    print('Username / Email not in database')
                    # the credentials are invalid
                    return None


@api.get("/login", auth=BasicAuth())
def basic(request):
    return {"httpuser": request.auth}

Labeled my Login in the Django urls.py:

from django.urls import path, include
from account import views

urlpatterns = [
    path('login', views.login, name='login_page'),
    path('profile', views.profile, name='profile_page'),
    path('register', views.register, name='register_page'),
]

Then on my dash side of things I setup a method to send a request to the API:

import requests
import colorama


def login(username, password):

    url_login = 'http://127.0.0.1:8000/api/login'

    response = requests.get(url_login, auth=(username, password))


    print(colorama.Fore.YELLOW + f"{response.status_code}")
    print(colorama.Fore.WHITE + f"{response.headers}")
    print(colorama.Fore.GREEN + f"{response.content}")
    print(colorama.Fore.RESET + response.text)
    if response.status_code == 200:
        print(colorama.Fore.GREEN + f"Login Successful")
        print(colorama.Fore.RESET)
        return True
    else:
        print(colorama.Fore.RED + f"Login Failed")
        print(colorama.Fore.RESET)
        return False


if __name__ == '__main__':
    login('test@gmail.com', 'NeverTellMeTheOdds')

Lastly on my app.py I created a callback event that calls the function login and based on the response changes the application to either login user or inform them they don’t have an account with us. Might be a little more complex than the default authentication offered with plotly but I’m after more features and figured I’d share this as you or someone in your same situation might find it useful.

1 Like

Hello @iianmr,

Basic Auth does have its limitations.

You can use something like @PipInstallPython mentioned, or you can use something like this:

Be sure you read through the comments.

Also, you can lock down the entire application of dash and expose only a simple login page from flask to create a logged in user for authentication for the application.

Using flask_login allows for some nice flexibility and customizations for the logged in user, as well as being able to log sessions and maybe even perhaps 2FA.

Basic Auth, anyone with the password gets in automatically, no verification, etc.

4 Likes

You can also monkey patch BasicAuth so it works with multi page:

from dash_auth import BasicAuth

# Monkey patch basic auth to work on non-index pages
def basic_auth_wrapper(basic_auth, func):
    """Updated auth wrapper to work on all pages rather than just index"""

    def wrap(*args, **kwargs):
        if basic_auth.is_authorized():
            return func(*args, **kwargs)
        return basic_auth.login_request()

    return wrap


BasicAuth.auth_wrapper = basic_auth_wrapper

Not sure why this is not the default behaviour in dash_auth but this does the trick. I could open a PR but looks like dash_auth hasn’t been updated since 2020 so not sure it’s worth it.

6 Likes

Thanks for the workaround @RenaudLN ! You’re right, we’ve somewhat ignored dash-auth - and a lot of our other peripheral packages - for the last year or so. In many cases we deemed them to be stable, but we should have more time to devote to these packages this year. So I’ll take a look at the other open PRs in dash-auth (like dash v2 compatibility), and if you’d like to make a PR to update BasicAuth I’ll make sure we take a look.

TBH if I were writing BasicAuth today, rather than a wrapper around certain view functions I’d probably make it a before_request handler. I think you’re right there’s not much benefit to distinguishing index from non-index views.

4 Likes

Very cool, thanks! I did to change Callable to callable, otherwise I get an error. But otherwise works perfectly, thank you!

@alexcjohnson I finally found time to do that PR :slight_smile: Make BasicAuth prompt for id/password on non-index pages. by RenaudLN · Pull Request #140 · plotly/dash-auth · GitHub

4 Likes