Dash App Pages with Flask Login Flow using Flask

Nice aspect of Dash security… But, what if we put protection inside @callback function also?

Sth like this also in callback…

if not current_user.is_authenticated:
    return html.Div(["Please ", dcc.Link("login", href="/login"), " to continue"])

What is your opinion?

Hi All

Authenticating username and passwords, i realised that if the developer is not careful we could land with a netflix effect. multiple users joining with one login details

Say you want to restrict exclusive access to your users but they are very kind and share their login details to others .

So I thought in an alternative way, the following are the steps:

  • Login page - > generic landing page
  • if username and password are not in records → go to generic landing page
  • if username and password match records:
    Generate a random string of characters and save in your username records, AND send the user to an url weblink with an address like… “www.webapp/username/random_string/”
  • Username lands back to the page with “webpage/username/random_string”.
    with a callback with Input(‘url’, ‘pathname’), read the pathname, split by “/” and catch the username, and the random key
  • check in the register that username has that specific key in your records and send to the customised page
  • in that way, if the users shares their login details to others, there wont be a chance of having two users at the same time

Another alternative option is a “passwordless” option. Sending automatic emails with a weblink that includes an specific created and saved string of random characters (which the app will check to match the registry once the users clicks on the link in their email, sending the user to the landing page with username and the random key). This case can be very simplistic and very safe if you think about it, user can only indicate their username and the app sends an automatic emails with a random key links for access. In this case security is passed on whichever email company the users has. Since the random key is generated every time a user gets in this option is very safe.

Hello @marcoV,

Thanks for bringing this up.

Using any of those methods, or flask_login by itself will not keep from people doing things to share the logins.

For example, someone could always forward the email that you are sending for the login or do other things. The default session cookie could also be shared and bypass the info. Thus why making sure your cookies are secure is a big thing.

Honestly, if you are wanting to have a subscription based product and using logins and you are concerned with people sharing the app, this is where your legal team needs to step in to make a:

  • privacy policy
  • end user license agreement
  • need to make sure there is a cookie consent

On top of this, because the user has accepted these things, and they use the app in a manner that is not along the agreement, then you can disable their account.

You will also need to be additionally monitoring their account for any activity outside of the norm. Locations aren’t in the same area, etc.

This is all if you mind people sharing their login.

You could also have something in there which limits the sessions to something like 3 to 5. Microsoft uses this for its licenses as well, so might be a good starting point. This would allow people to log in on their phone and computer and one other device.

You would also need a mechanism that checks whether their session should be logged out. :blush:

3 Likes

Hello @jinnyzor ,

This code is super helpful. I was able to lock down some pages and still leave some for anonyms user to access.
One thing I noticed:

I have NavLinks on the side which change the page of my multi pages app using the pages feature.
Even tough, I have dcc.Location with refresh and Navlinks with refresh, I won’t get a request I can intercept when switching between pages.

The idea is, that I have a second page, and if the user is not registered when switching from page 1 to page 2, he should get redirected to login.

Are the dash pages not routed through the request process?

They are routed through a post request to an endpoint for dash component updates.

You can intercept by looking for the pathname attribute on the inputs.

1 Like

Ah! No wonder. I was only looking at the get. Thank you!

1 Like

Does the redirect work the same way with the Get request?

What i did looked like this:

@app.before_request
    def check_login():
        block_list = ["/composition"]
        # Check if the requested route is whitelisted
        if request.method == "GET":
            if current_user:
                if request.path not in block_list:
                    return
                elif request.path in block_list and not current_user.is_authenticated:
                    return redirect(url_for("auth_bp.login"))
                else:
                    for pg in dash.page_registry:
                        if request.path == dash.page_registry[pg]["path"]:
                            session["url"] = request.url
                        return
            return
        else:
            if current_user:
                data = json.loads(request.data)
                # Check if the request data contains 'inputs' with a 'pathname' attribute
                inputs = data.get('inputs', [])

                if len(inputs) == 1 and inputs[0].get('id', False) == 'url' and inputs[0].get('value', False) in block_list and not current_user.is_authenticated:
                    # The page redirect goes to block list and user is not authenticated, redirect
                    session["url"] = request.url
                    return redirect(url_for("auth_bp.login"))
                else:
                    for pg in dash.page_registry:
                        if request.path == dash.page_registry[pg]["path"]:
                            session["url"] = inputs[0].get('value', False)
                        return
            return jsonify({"status": "401", "statusText": "unauthorized access"})

The post request will need to be responded to differently.

If it is to the dash update endpoint, then it needs to be returned like:

Otherwise, the redirect can be a regular flask redirect.

Can I use the @before_request in the Post part also to redirect to an external page?

jsonify({"multi": True, "response": {"url": {"pathname": "/login"}}})

I tried to just stet the pathname different, which did not work :smiley:

Depends on the endpoint, if the fetch is not from dash then it won’t understand.

The fetch would have to allow for following redirects I think.

HI @jinnyzor , Somehow I ended at this Problem again.

My issue is:

The session cookie expires and the user is set to anonymous with flask_login. In my before request I also see it, but as long as the request is coming from within dash, for example clicking a button, then the redirect does not work.

   @app.before_request
    def handle_before_request():
        if request.method == "GET":
            found = ("admin" in request.path) and ("static" not in request.path)
            if current_user.is_authenticated:
                if found:
                    if current_user.is_admin:
                        from src.helpers import save_last_page_visited

                        save_last_page_visited(request.path)

                if not current_user.is_admin and request.path in [
                    current_app.config["URL_PAGE_1"],
                    current_app.config["URL_PAGE_2"],
                ]:
                    flash(
                        "This page is for admins only. Please Sign in as admin so you can check this page!",
                        category="warning",
                    )
                    return redirect("/login")
        else:

            if current_user.is_anonymous and request.path == "/_dash-update-component":
                #Here should the user be redirected to login page
                # return jsonify({"multi": True, "response": {"url": {"pathname": "/login"}}})
                return redirect("/login")

The login flow is done in flask, where the user can either use his account made on my page, or google. I store the user in the session on login. That all works well.

If I use neither works for me return jsonify({"multi": True, "response": {"url": {"pathname": "/login"}}}) or redirect("/login").

Do you know how I can redirect the user correctly?

Hello @simon-u,

In your get request, you aren’t redirecting if the user isn’t authenticated. This would be done with an else statement.

As far as the post request, you don’t want to redirect if the user isn’t logged in and the path isn’t for dash?

Also, for redirecting a user for a page request, you need to return for the location, I don’t know if you have url as a component in your app.

If you are using pages, you can target _pages_location instead of passing url.

@jinnyzor

  1. Yes , I had the else statement there before. Need to check it also because we have some URL outside the protected area, like redirecting to google Oauth and coming back.

  2. In the post request, I want to redirect the user, as long as he is not logged in, most likely he will be within dash. The issue was, that the user stayed on one page and the session timed out. Now he is pressing a button (not a link to a different page) and nothing happens because he is not logged in any more. In that case, I want to redirect him.

  3. Yes, I have the URL as a component. The login URL is outside of dash and handled by flask. Similar as in your example above.

To confirm, when referring to app it is the flask server and not the dash app?

Yes, in my code snippet app is the flask server not the dash app/