Search code examples
pythonformsflaskflask-login

How do I pass through the "next" URL with Flask and Flask-login?


The documentation for Flask-login talks about handling a "next" URL. The idea seems to be:

  1. User goes to /secret
  2. User is redirect to a login page (e.g. /login)
  3. After a successful login, the user is redirected back to /secret

The only semi-full example using Flask-login that I've found is https://gist.github.com/bkdinoop/6698956 . It's helpful, but since it doesn't include the HTML template files, I'm seeing if I can recreate them as an self-training exercise.

Here's a simplified version of the /secret and /login section:

@app.route("/secret")
@fresh_login_required
def secret():
    return render_template("secret.html")

@app.route("/login", methods=["GET", "POST"])
def login():
    <...login-checking code omitted...>
    if user_is_logged_in:
        flash("Logged in!")
        return redirect(request.args.get("next") or url_for("index"))
    else:
        flash("Sorry, but you could not log in.")
        return render_template("login.html")

And here's login.html:

<form name="loginform" action="{{ url_for('login') }}" method="POST">
Username: <input type="text" name="username" size="30" /><br />
Password: <input type="password" name="password" size="30" /><br />
<input type="submit" value="Login" /><br />

Now, when the user visits /secret, he gets redirected to /login?next=%2Fsecret . So far, so good - the "next" parameter is in the query string.

However when the user submits the login form, he is redirected back to the index page, not back to the /secret URL.

I'm guessing the reason is because the "next' parameter, which was available on the incoming URL, is not incorporated into the login form and is therefore not being passed as a variable when the form is processed. But what's the right way to solve this?

One solution seems to work - change the <form> tag from

<form name="loginform" action="{{ url_for('login') }}" method="POST">

to:

<form name="loginform" method="POST">

With the "action" attribute removed, the browser (at least Firefox 45 on Windows) automatically uses the current URL, causing it to inherit the ?next=%2Fsecret query string, which successfully sends it on to the form processing handler.

But is omitting the "action" form attribute and letting the browser fill it in the right solution? Does it work across all browsers and platforms?

Or does Flask or Flask-login intend for this to be handled in a different way?


Solution

  • If you need to specify a different action attribute in your form you can't use the next parameter provided by Flask-Login. I'd recommend anyways to put the endpoint instead of the url into the url parameter since it is easier to validate. Here's some code from the application I'm working on, maybe this can help you.

    Overwrite Flask-Login's unauthorized handler to use the endpoint instead of the url in the next parameter:

    @login_manager.unauthorized_handler
    def handle_needs_login():
        flash("You have to be logged in to access this page.")
        return redirect(url_for('account.login', next=request.endpoint))
    

    Use request.endpoint in your own URLs too:

    {# login form #}
    <form action="{{ url_for('account.login', next=request.endpoint) }}" method="post">
    ...
    </form>
    

    Redirect to the endpoint in the next parameter if it exists and is valid, else redirect to a fallback.

    def redirect_dest(fallback):
        dest = request.args.get('next')
        try:
            dest_url = url_for(dest)
        except:
            return redirect(fallback)
        return redirect(dest_url)
    
    @app.route("/login", methods=["GET", "POST"])
    def login():
        ...
        if user_is_logged_in:
            flash("Logged in!")
            return redirect_dest(fallback=url_for('general.index'))
        else:
            flash("Sorry, but you could not log in.")
            return render_template("login.html")