How to handle with redirect in Dash app?

I made a Keycloak integration to the Dash app. And there is the logic that if a session expires, for any request from the frontend, the backend will return a redirect to the auth URL. I’m obtaining the next case: session is expired, a user makes some action on the page (for example use dropdown) and the frontend sends “POST https://app_url/_dash-update-component” request, but instead of a familiar response as JSON, receives redirect and can’t handle it. For a user, it looks like the app is broken and he has to refresh the page. Is there any way to proceed with a redirect?

dash 2.11.1, dcc 2.11.0

1 Like

I am struggling with the same problem. Have you found a solution?

Also interested, if anyone has a good solution :blush:

from flask import Flask, redirect, request, session  
import dash 
import json
from dash import dcc, html, Input, Output

# Initialize Flask and Dash apps  
server = Flask(__name__)  
server.secret_key = 'your_secret_key'  # Used to protect the session  
app = dash.Dash(__name__, server=server, url_base_pathname='/')  
  
# Let's assume this is the function to check the user's logged-in status  
def user_is_logged_in(): 
    return False
    #return 'user' in session  

@server.before_request    
def before_request_func():    
    # Check the request path and method    
    if request.path == '/_dash-update-component' and request.method == 'POST':    
        # Check if the user is logged in    
        if not user_is_logged_in():
            if 'redirected' not in session:
                session['redirected'] = True
            # If the user is not logged in, return a JSON response to redirect to /login    
                return json.dumps({  
                    "multi": True,  
                    "response": {  
                        "page-content": {  
                            "children": {  
                                "props": {'id': 'url', 'href': '/login'},  
                                "type": "Location",  
                                "namespace": "dash_core_components"  
                            }  
                        }  
                    }  
                })  
  
# Dash app's layout and callbacks  
app.layout = html.Div([  
    dcc.Location(id='url', refresh=False),  
    html.Div(id='page-content')  
])  
  
@app.callback(Output('page-content', 'children'),  
              [Input('url', 'pathname')])  
def display_page(pathname):  
    if pathname == '/':  
        return html.Div([  
            # Page content  
        ])  
    elif pathname == '/login':  
        return html.Div([  
            # Login page content  
        ])  
    # Content for other pages  
  
# Run the server  
if __name__ == '__main__':  
    app.run(debug=False)  

Here is the minimal code that may work well, For your reference.

from flask import Flask, redirect, request, session
import datetime
import dash
import json
from dash import dcc, html, Input, Output

# Initialize Flask and Dash apps
server = Flask(__name__)
server.secret_key = 'your_secret_key'  # Used to protect the session
server.config['PERMANENT_SESSION_LIFETIME'] = datetime.timedelta(seconds=30)
app = dash.Dash(__name__, server=server, url_base_pathname='/')


# Let's assume this is the function to check the user's logged-in status
def user_is_logged_in():
    return bool(session.get('user'))


@server.route('/login', methods=['GET', 'POST'])
def login_page():
    session.permanent = True
    if request.method == "POST":
        session['user'] = f"{request.form['username']}:{request.form['password']}"
        return redirect('/')
    else:
        return ('<form method="post">'
                '<input type="text" name="username" id="un" title="username" placeholder="username"/>'
                '<input type="password" name="password" id="pw" title="username" placeholder="password"/>'
                '<button type="submit">Login</button>'
                '</form>')


@server.before_request
def before_request_func():
    # Check if the user is logged in
    if not user_is_logged_in() and request.path != '/login':
        # Check the request path and method
        if request.path == '/_dash-update-component' and request.method == 'POST':
            # If the user is not logged in, return a JSON response to redirect to /login
            return json.dumps({
                "multi": True,
                "response": {
                    "page-content": {
                        "children": {
                            "props": {'id': 'url', 'href': '/login'},
                            "type": "Location",
                            "namespace": "dash_core_components"
                        }
                    }
                }
            })
        else:
            return redirect('/login')


d = {v: v.capitalize() for v in ['a', 'b', 'c', 'd']}

app.layout = html.Div([
    dcc.Location(id='url', refresh=True),
    html.Div(
        [
            html.Label('Labels'),
            dcc.Dropdown([{'label': v, 'value': k} for k, v in d.items()],
                         value=None, id='dropdown'),
            html.Div('Select one of the label'),
        ],
    ),
    html.Div(id="indicator"),
])


@app.callback(
    Output('indicator', 'children'),
    [Input('dropdown', 'value')],
    prevent_initial_call=True)
def update_indicator(val):
    return d[val]


# Run the server
if __name__ == '__main__':
    server.run(port=5008)

I took a look at your example, but unfortunately, it doesn’t respond to my case. I leave minimal example of my case. As you can see, it has a session lifespan, and when session expires, and a user uses dropdown nothing happens, but in network you can see that backend returns the corresponding JSON to change location. Could you look at my case?

Hi, yeah, your suggestion looks like a solution, but unfortunately, I don’t know js very well how to write the correct interception. If somebody will write what should I add - it would be nice. Also, from the backend, I might add the current location value to all my callbacks, but this solution looks weird.

You need to adjust your code based on your app’s layout. I made some adjustment so that this can work for your case.

import datetime

import dash
from dash import Input, Output, dcc, html
from flask import Flask, jsonify, redirect, request, session

# Initialize Flask and Dash apps
server = Flask(__name__)
server.secret_key = "your_secret_key"  # Used to protect the session
server.config["PERMANENT_SESSION_LIFETIME"] = datetime.timedelta(seconds=30)
app = dash.Dash(__name__, server=server, url_base_pathname="/")


# Let's assume this is the function to check the user's logged-in status
def user_is_logged_in():
    return bool(session.get("user"))


@server.route("/login", methods=["GET", "POST"])
def login_page():
    session.permanent = True
    if request.method == "POST":
        session["user"] = f"{request.form['username']}:{request.form['password']}"
        return redirect("/")
    else:
        return (
            '<form method="post">'
            '<input type="text" name="username" id="un" title="username" placeholder="username"/>'
            '<input type="password" name="password" id="pw" title="username" placeholder="password"/>'
            '<button type="submit">Login</button>'
            "</form>"
        )


@server.before_request
def before_request_func():
    # Check if the user is logged in
    if not user_is_logged_in() and request.path != "/login":
        # Check the request path and method
        if request.path == "/_dash-update-component" and request.method == "POST":
            # If the user is not logged in, return a JSON response to redirect to /login
            return jsonify({"multi": True, "response": {"url": {"pathname": "/login"}}})
        else:
            return redirect("/login")


d = {v: v.capitalize() for v in ["a", "b", "c", "d"]}

app.layout = html.Div(
    [
        dcc.Location(id="url", refresh=True),
        html.Div(
            [
                html.Label("Labels"),
                dcc.Dropdown(
                    [{"label": v, "value": k} for k, v in d.items()],
                    value=None,
                    id="dropdown",
                ),
                html.Div("Select one of the label"),
            ],
        ),
        html.Div(id="indicator"),
    ]
)


@app.callback(
    Output("indicator", "children"),
    [Input("dropdown", "value")],
    prevent_initial_call=True,
)
def update_indicator(val):
    return d[val]


# Run the server
if __name__ == "__main__":
    server.run(port=5008)

2 Likes

That is what I need, works perfect, thank you very much! Moreover, it works perfect on my real app! How did you get the way the response should look? I didn’t see that topic in the docs.

You can simply write a mockup callback that will trigger dcc.location’s value change and see what response on /_dash-update-component will get.

Yeah, that sounds good, but I was sure that the response should contain only corresponding properties to the request, but as I see now, dash’s frontend can handle with any valid response.

Thank you for this recommendation! I implemented something similar to redirect from dash to our OIDC authenticate URL and it works great.

Hi there, I added this functionality to dash-flask-keycloak package which order to easily integrate the Keycloak into a Flask/Dash applications. Feel free to text me if you have any questions.