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

Question:

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?

Asked By: David White

||

Answers:

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")
Answered By: timakro

@timakro provides a neat solution.
If you want to handle a dynamic link such as

index/<user>

then using url_for(request.endpoint,**request.view_args) instead because request.endpoint will not contain the dynamic vairable info:

 @login_manager.unauthorized_handler
 def handle_needs_login():
     flash("You have to be logged in to access this page.")
     #instead of using request.path to prevent Open Redirect Vulnerability 
     next=url_for(request.endpoint,**request.view_args)
     return redirect(url_for('account.login', next=next))

the following code shall be changed to:

def redirect_dest(home):
    dest_url = request.args.get('next')
    if not dest_url:
        dest_url = url_for(home)
    return redirect(dest_url)

@app.route("/login", methods=["GET", "POST"])
def login():
    ...
    if user_is_logged_in:
        flash("Logged in!")
        return redirect_dest(home=anyViewFunctionYouWantToSendUser)
    else:
        flash("Sorry, but you could not log in.")
        return render_template("login.html")
Answered By: Kurumi Tokisaki

Just in case someone is trying to pass through the “next” URL with Flask-Login but with Flask_Restful, the workaround I’ve been using is passing the “next” argument from the GET method to the POST method. With flask_restful the “next_page” argument is set to “None” after clicking on the Login Button in the login.html

login.html

...
<!-- next_page came from "render_template(next_page=request.args.get('next') ...)" in the get() function -->
<!-- And also from render_template('login.html', next_page=next_page) in the post() function -->
<form action="{{ url_for('login', next=next_page) }}" method="POST" >
    <div class="field">
        <div class="control">
            <input class="input is-large" type="email" name="email" placeholder="Your Email" autofocus="">
        </div>
    </div>

    <div class="field">
        <div class="control">
            <input class="input is-large" type="password" name="password" placeholder="Your Password">
        </div>
    </div>
    <div class="field">
        <label class="checkbox">
            <input type="checkbox" name="remember_me">
            Remember me
        </label>
    </div>
    <button class="button is-block is-info is-large is-fullwidth">Login</button>
</form>
...

auth.py

class Login(Resource):

    def get(self):
        if current_user.is_authenticated:
            return redirect(url_for('home'))

        headers = {'Content-Type': 'text/html'}

#1 -->  # Here I pass the "next_page" to login.html
        return make_response(render_template('login.html', next_page=request.args.get('next')), 200, headers)

    def post(self):
#2 -->  # After the user clicks the login button, I retrieve the next_page saved in the GET method
        next_page = request.args.get('next')

        if current_user.is_authenticated:
            return redirect(url_for('home'))

        # Check if account exists in the db
        existing_account = Account.objects(email=request.form.get('email')).first()

        # Only redirects when the URL is relative, which ensures that the redirect 
        # stays within the same site as the application.
        if existing_account:
            if existing_account.check_password(request.form.get('password')):
                login_user(existing_account, remember=request.form.get('remember_me'))

                if not next_page or url_parse(next_page).netloc != '':
                    return redirect(url_for('home'))

#3 -->          # Here I use the retrieved next_page argument
                return redirect(url_for(next_page))

        # Account not recognized
        flash('Please check your login details and try again.')
        headers = {'Content-Type': 'text/html'}

#4 -->  # I also pass the "next_page" here in case the user-provided data is wrong
        return make_response(render_template('login.html', next_page=next_page), 200, headers)
Answered By: prietosanti

Out of all the above answers I didn’t find anything helpful for me.
But what I found, I will share with you..

In login.html just add the below code

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

And login definition changes as follows

@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")

And This worked perfectly for me…

Thank you …

Answered By: Vishal N

This is what worked for me, this is an addition to what @timakro and @Kurumi Tokisaki has provided in an earlier post.

@login_manager.unauthorized_handler
def handle_needs_login():
    return redirect(url_for('login', next_page=request.endpoint))

@app.route('/login/<next_page>', methods=['GET', 'POST'])
def login(next_page):
    if current_user.is_authenticated:
        return redirect(url_for(next_page))

    if is_a_successfull_login():
        redirect(url_for(next_page))
Answered By: Rusiru Boteju

Simple Example

@app.route('/login', methods=['GET', 'POST'])
def login():
    // do your stuff
    next_page = request.form.get('next_page')
    # next_page either be 'http://localhost:5000/None or 'http://localhost:5000/some_endpoints
   return redirect(next_page) if 'None' not in next_page else redirect(url_for('default_action'))

Note

as Mentioned in Flask documentation
don’t forget to pass next_url in html

<input type="hidden" value="{{ request.args.get('next') }}" name="next_page" id="next_page"/>
Answered By: Azhar Uddin Sheikh
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.