0

Setup:

  1. Flask web app.
  2. User navigates to one of multiple pages [e.g. game page (localhost:5000/game) or players page (localhost:5000/players)] through a post request with GAME_ID in the form. GAME_ID is used to fetch additional details and render the page.

Objective:

  1. When an unauthenticated users makes a post request to /game, redirect them to the /login page but retain the endpoint (game) and the GAME_ID in the session, so that on successful login, I can send them back to the page they were accessing. My approach was to make a 307 redirect.

Approach (based on this and this):

In application.py

@login_manager.unauthorized_handler
def intercept_unauthorized():
    <Set NEXT_PAGE_FOR_REDIRECT and GAME_ID_FOR_REDIRECT into the session>
    return redirect(url_for('login'))

@application.route("/login", methods=['GET', 'POST'])
def login():
    #User sees the login page
    if request.method=='GET':
      <show the login page>
    
    #User has submitted username and password on the login page
    elif request.method=='POST':
      <authentication code here>
      if validUser:
          #Get the page the user was trying to access e.g. 'game'
          nextPage = session.get('NEXT_PAGE_FOR_REDIRECT')
          session.pop('NEXT_PAGE_FOR_REDIRECT')
          gameID = session.get('GAME_ID_FOR_REDIRECT')        
          session.pop('GAME_ID_FOR_REDIRECT')

          return redirect(url_for(next_page, gameID=gameID), code=307)

@application.route("/game", methods=['POST'])
@login_required
def game():
    if 'gameID' in request.form:
        render_template('game.html', gameID=request.form['gameID'])

Issue:

  1. I am able to redirect to /game where request.method is POST. But, the data in request.form is the data submitted from the login page submit i.e. username and password.
  2. I am able to access the gameID I set in the url_for line, but it is sent in request.args (and is visible in the browser's address bar) instead of through request.form.

Questions:

  1. Is my approach of using a redirect correct?
  2. How can I modify the form being sent to /game so that I can add gameID to it?
VRA
  • 91
  • 9

1 Answers1

0

From: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307

HTTP 307 Temporary Redirect redirect status response code indicates that the resource requested has been temporarily moved to the URL given by the Location headers.

The method and the body of the original request are reused to perform the redirected request. In the cases where you want the method used to be changed to GET, use 303 See Other instead. This is useful when you want to give an answer to a PUT method that is not the uploaded resources, but a confirmation message (like "You successfully uploaded XYZ").

The only difference between 307 and 302 is that 307 guarantees that the method and the body will not be changed when the redirected request is made.

So, the original request.form is retained. You could just use the session to display the hidden ID, like:

@application.route("/game", methods=['POST'])
@login_required
def game():
    gameID = session.get('GAME_ID_FOR_REDIRECT')        
    session.pop('GAME_ID_FOR_REDIRECT')
    render_template('game.html', gameID=gameID )

Of course, you'd have to remove the pop() from login()

GAEfan
  • 11,244
  • 2
  • 17
  • 33
  • Sorry I failed to mention this in my question. I had tried the _method approach of forcing a POST, but it still sent gameID as a query parameter. I want to avoid exposing the gameID on the UI and so want to use a POST only on the /game page. The first two suggestions you gave both will cause the gameID (e.g. 123) to be visible in the address bar (either as /game/123 or as game?gameID=123). – VRA Aug 11 '20 at 16:57
  • Also, I didn't understand two elements of your response. 1. "Wants to send a GET" <<< why's that? Isn't the 307 code explicitly meant for retaining the request.method? Since, the request in `login` that redirects to `\game`is also a POST, it shouldn't even try to create a GET, right? So I don't understand the part about _switching_ to POST and losing parameters. – VRA Aug 11 '20 at 17:02
  • 2. You said the endpoint cannot resolve `gameID`. But in case of a POST request where gameID would be contained in the `request.form` I don't need to specify gameID as part of the end point, right? All I need to do is read it from the `request.form`. – VRA Aug 11 '20 at 17:03
  • You are correct. I edited. I did not know that the 307 retained the original body and method. – GAEfan Aug 11 '20 at 17:15
  • My original approach was, as you suggested now, to put gameID in the session too. The reason I dropped that approach is not being able to refresh the page. When the gameID is sent through a POST request, in the form (the primary way in which a logged in user arrives at `/game`), refreshing the page retains the request and hence `game()` finds the gameID in the request form again. But if I fetch gameID from the session, in case of a redirect, then on refreshing the page, `game()` won't find gameID because it has been, correctly, popped when it was first fetched. – VRA Aug 11 '20 at 17:27
  • Btw, thanks for the quick responses, @gaefan! Really appreciate this. – VRA Aug 11 '20 at 17:28
  • 1
    Perhaps don't `pop()`? The only other solution I can think of would be to add `gameID` as a hidden field in `login()`. Then, it would be in the form. Or to build a redirect outside of flash where you can build the payload. – GAEfan Aug 11 '20 at 17:32
  • I was trying to avoid put hidden data fragments on the login page. But seems like that's the only feasible option. I'll try this out and update how it works out. – VRA Aug 11 '20 at 23:53
  • Now, I'm putting the `gameID` into a hidden field on the `login` page so that it gets embedded in the form received from the `login` POST. Then I do a 307 redirect to `/game`. I am able to get the gameID from the form, but only the first time. When I refresh the page, the form is empty. In contrast, when I go to `/game` through the usual method i.e. a POST form submit from the UI (as mentioned in **Setup** point 2), refreshing works i.e. when I refresh the page, I get the `gameID` in `request.form`. – VRA Aug 14 '20 at 19:03
  • This whole idea of redirecting the user to avoid reposting is flawed, IMHO. First, how often is this a problem? Almost never. A better way would be to put an ajax login on the page (if not logged in), and stay on `/game` – GAEfan Aug 14 '20 at 19:16
  • Well, the scenario is that if the user has gone to `/game` and leaves the tab open for long enough to get timed out, the next time they perform an action on the page they will be taken to the login page, which is the correct flow. What I want to achieve is on logging in, they should be taken back to the page they were viewing, instead of the home page. – VRA Aug 14 '20 at 19:51
  • I'm HAVING to use the redirect because all this is happening in python. Is there a way to trigger a POST request from python itself? As in, from the POST section of `/login`, is there a way to create a new POST request to `/game` and set a form in this new request? – VRA Aug 14 '20 at 19:53
  • Apparently there's [this](https://requests.readthedocs.io/en/master/user/quickstart/). I'm going to experiment with this. – VRA Aug 14 '20 at 19:57