Search code examples
pythonflaskflask-login

Flask-Login Redirecting to React Client after User has been logged in


I am trying to integrate flask-login with a React application. After the user has been logged in, it appears that flask-login enforces a page redirect.

Since we don't want to redirect to the same page we have to make sure that the actual back redirect is slightly different (only use the submitted data, not the referrer)

Source: https://web.archive.org/web/20120504074405/http://flask.pocoo.org/snippets/62

The difference between what the documentation is showing and what I'm doing is that my client is on a different domain (thus, I will be faced with CORS problems), whereas the documentation's client and server (I assume) on the same domain.

This is evident in this code snippet:

def is_safe_url(target):
    ref_url = urlparse(request.host_url)
    test_url = urlparse(urljoin(request.host_url, target))
    return test_url.scheme in ('http', 'https') and \
           ref_url.netloc == test_url.netloc

From above, the function returns true if ref_url.netloc == test_url.netloc (I left out the other portion for simplicity). I looked into the attribute of netloc which turns out to be the domain name without the top-level domain. For example, if the domain name is: domain.com, then the netloc would be domain.

However, for my scenario, that won't work.

For example, frontend: www.frontend.com backend: www.backend.com

The request to login to backend should redirect back to say, www.frontend.com/home. Based off the code presented in the documentation, it would never redirect to the intended destination. So I tried to modify the code a bit and got this:

def is_safe_url(request, target):
    ref_url = urlparse(request.host_url)
    test_url = urlparse(urljoin(request.host_url, target))
    return test_url.scheme in ['http', 'https'] and \
           (test_url.netloc == 'localhost:3000' or \
            ref_url.netloc == test_url.netloc 
           )

I figured if I configure the redirect in a way where it checks for a suitable domain (in my example, it would be localhost:3000) and check for domains that are not allowed, then there shouldn't be any security threats here. However, please correct me if I'm missing something.

Sadly, this results in a CORS issue.

CORS

I would also like to add that most of the examples provided by flask-login is using jinja templating. Something that isn't very clear from the documentation is that should redirects even be used if you aren't using them?

Rest of the code:

def is_safe_url(request, target):
    ref_url = urlparse(request.host_url)
    test_url = urlparse(urljoin(request.host_url, target))
    return test_url.scheme in ['http', 'https'] and \
           (test_url.netloc == 'localhost:3000' or \
            ref_url.netloc == test_url.netloc 
           )

def get_redirect_target(request):
    for target in request.values.get('next'), request.referrer:
        print('target', target)
        if not target:
            continue
        if is_safe_url(request, target):
            return target

@user_bp.route('/login', methods=['GET', 'POST'])
def login():
    next = get_redirect_target(request)
    if current_user.is_authenticated:
        return jsonify({ 'login': True })
    if request.method == 'POST':
        data = request.get_json()
        form_input = ImmutableMultiDict(data)
        form = UserAuthenticationForm(form_input)        
        if form.validate():
            try:
                user = RegisteredUser.query.filter_by(email = data['user']).first() or RegisteredUser.query.filter_by(username = data['user']).first()
                if user is not None and user.check_password(data['password']):
                    login_user(user)
                    response = redirect(next)
                    print(response.get_data())
                    return response
                    # return jsonify({ 'login': True })
            except Exception as e: # Handle a faulty connection to the database
                print('Error: ' + str(e))
        return jsonify({ 'login': False })

Edit:

CORS Setting:

CORS(app, origins=["http://localhost:3000"], expose_headers=["Content-Type", "X-CSRFToken"], supports_credentials=True)

Solution

  • So I think you're first issue is that you're mixing multi-page web app flow with single page web app flow.

    In multipage web apps once you handle the user logging in you normally redirect them to another page so they can start using your app. But in single page web apps you usually dont redirect but instead send a token or something that the client will hold onto that will authenticate them later when they make a future request. For SPAs the app itself dictates the flow of how the user uses the app. For MPAs the server dictates the flow via redirects or other mechanisms.

    -A side note, redirect is used in situations like OAuth for authentication for SPAs.

    Also in the code in that blog is showing how to do a safe redirect but really its also doing something else. The next input that is in the form is a way to store the page where the user came from so that after they login you can give them a better user experience and redirect them back to what they wanted. Example I have a bookmark to a page that I use everyday however my login session as expired. So the backend saves that url to next and redirects me to the login page and adds next to the page so that once I login next will be sent to the server and now it can redirect me back to the bookmarked page. In react we dont need this cause I can store this info in the app itself and then once I log in I can show the appropriate page via react code.

    Now you are wanting to use flask-login which is fine. What you need to do is in your react app do a javascript post request to the backend at the login endpoint. So backend.com/login for example as the login endpoint. When the login is successful you should ignore the default redirect behavior and grab the id that is returned in the session header. From now on you use that id as a session header on all future requests and everything should work fine.

    Now you do have CORS to work with. So you have two servers. frontend.com and backend.com. frontend.com serves your react app and backend.com is the api and authentication.

    You need to enable CORS on backend.com and whitelist frontend.com. This will tell backend.com that any requests from a client that originated from frontend.com is fine and should be processed. Else it should return a CORS error.