When a logged-out user wants to access a protected page normally we'll redirect them to the login page in our application. However, after logging in, many applications will send users to their "profile" or "dashboard" page, instead of where they initially wanted to go.

That can be quite frustrating, so let's learn how to provide users with a better experience when using our pages.

Prerequisites

For this blog post you'll need a Flask application that has login functionality, but doesn't yet handle this flow correctly.

In addition, you should read our previous blog post in the series, "Protecting endpoints in Flask apps by requiring login". There we cover how to code the decorator which we'll be modifying in this post.

In case you don't have a sample Flask app handy, I've coded one for you. You can access it here. To test this flow, launch the application and:

  1. Access the /profile endpoint. This endpoint has been configured to require login, and will redirect you to the login page.
  2. Log in using the test user details, jose and 1234.
  3. Notice that now you end up at /, not at /profile. This is what we'll be fixing in this blog post.

Telling the login endpoint where to send users next

In order for our app to be able to redirect users to the right place, the /login endpoint must know what that place is. Therefore, we need to be able to tell it where users want to end up as they log in.

Similarly, in order for the /login page to know where users want to end up, it must be told what that final destination is.

Therefore the flow of data will go like this:

  1. User wants to access /profile. This is protected, so we redirect users to the /login page together with what the final destination is.
  2. User logs in, so they submit the login form and in that submission we include what the final destination is.
  3. We process the login in our Python code, and instead of redirecting users to the default destination, we redirect them to their intended destination.

Let's begin by doing the first step: telling the /login page what the final destination is. To do so, we'll modify login_required decorator.

At the moment the decorator looks like this:

def login_required(func):
    @functools.wraps(func)
    def secure_function(*args, **kwargs):
        if "email" not in session:
            return redirect(url_for("login"))
        return func(*args, **kwargs)

    return secure_function

But we want to add a piece of data to be sent to the /login page:

def login_required(func):
    @functools.wraps(func)
    def secure_function(*args, **kwargs):
        if "email" not in session:
            return redirect(url_for("login", next=request.url))
        return func(*args, **kwargs)

    return secure_function

This means that when we redirect users to the login page, instead of sending them to /login, we'll send them to /login?next=/profile. Because we're including that data in the URL, it means that the page itself can access that information while it is being rendered.

Passing that information when a user logs in

When a user submits the form though, our Python code will no longer have access to ?next=/profile. We must pass that information in inside the form.

The only way to do that is to include a field inside the form that contains the final destination as text. It's a bit hacky, but we can do this without disturbing the user flow by adding a hidden field to our form.

Let's modify our login form and include this field inside it:

<input
    type="hidden"
    name="next"
    value="{{ request.args.get('next', '') }}"
/>

What this does is it reads the next argument from the query string parameters and includes its value in a hidden field. When we submit the form, we'll have access to this field in our Python code and we'll be able to use its value to redirect the user.

Note this field doesn't need a label because it is hidden, so users won't be able to see it on the page.

Using the hidden field to redirect the user

Finally, when we receive the hidden field value we must use it to redirect the user to their final intended destination.

Remember this field might be empty if the user isn't trying to access a particular site, so we only want to redirect the user to the location in this field if there is a value—otherwise we'll still redirect to the default destination (which is / at the moment).

At the moment our login handler code looks like this:

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form.get("username")
        password = request.form.get("password")

        if username in users and users[username][1] == password:
            session["username"] = username
            return redirect(url_for("profile"))
    return render_template("login.html")

We'll change it to this:

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form.get("username")
        password = request.form.get("password")
        next_url = request.form.get("next")

        if username in users and users[username][1] == password:
            session["username"] = username
            if next_url:
                return redirect(next_url)
            return redirect(url_for("profile"))
    return render_template("login.html")

What this does is read the contents of the hidden field and use that to redirect the user.

Note that we don't need to call url_for with next_url because it already is an URL in our application, and not an endpoint handler.

Recap

To recap, in order to redirect users to their intended destination instead of the default "logged in" page, we must:

  1. Pass in the final destination to the log in page.
  2. From there, pass in the final destination to the log in handler.
  3. Use that to redirect users instead of sending them to the default destination page.

Said like that, it seems quite obvious—but it's always tricky to remember that every endpoint is completely independent and we need to pass data through the user's browser whenever we want to do this type of communication.

If you want to check out the final, complete code after this blog post, use this link!

And if you'd like to read the previous posts on this topic, which tackle more foundational elements of Flask web app development, read them here:

Wrapping Up

Thank you for reading! I hope you've found this post interesting and useful.

If you want to learn more about web development with Flask, check our our Complete Python Web Course. If you're newer to Python and would like a more complete understanding of the fundamentals (including decorators!), check our Complete Python Course.

Thanks for reading, and I'll see you next time!