vimalloc / flask-jwt-extended

An open source Flask extension that provides JWT support (with batteries included)!
http://flask-jwt-extended.readthedocs.io/en/stable/
MIT License
1.54k stars 241 forks source link

Failure of POST request to a Flask Restful API using Jwt Extended (problem with the jwt required decorator) #174

Closed zakizem closed 6 years ago

zakizem commented 6 years ago

Hi everybody,

I'm using jwt extended with flask restful, and I have on the client side, http requests that consume my API.

The problem is when I send a POST request (that has a json body) to a protected endpoint, sometimes I get the response i'm waiting for (a 401 Unauthorized or a 200), but a lot of times (90% of the time) the request fails and I get in the Chrome debugger of the request Header 'Provisional headers are shown'.

Please note that this only happens on CHROME and it works fine in Firefox. And it also only happens when I send data with my request, if I send a post request without Json content, everything's also fine.

We would think at first that the problem isn't coming from the API but from the client, but when I take of the JWT REQUIRED decorator from my endpoint, the request doesn't fail 100% of the time.

Here's my code:

My API endpoint:

class InsertionReponses(Resource):

    decorators = [jwt_required]

    def post(self):

        # print(request)

        # data=request.get_json(force=True)

        return 'something'

My request from the JavaScript client (I'm using Vue JS Framework):

  var xmlHttp = new XMLHttpRequest();

  xmlHttp.open("POST", "http://127.0.0.1:5000/envoiReponses", true);

  xmlHttp.setRequestHeader("Content-Type", "application/json");

  xmlHttp.withCredentials = true;

  xmlHttp.send(JSON.stringify(someJsonData));

And here is what happens to my request in Chrome (when it fails, which happens most of the time)

provisional

vimalloc commented 6 years ago

I am positive the has something to do with chrome and not something that is setup incorrectly in this extension. Maybe cookies + CORS related, or a chrome extension causing problems or something? After doing some quick looking, it looks like you could try looking at the chrome://net-internals/#events page to potentially get more information on why the browser is blocking your request.

vimalloc commented 6 years ago

Maybe setup a webpack proxy for your vue-> flask communication instead of going to a url with a different port for your api compared to the url you load the html from. I’m not 100% sure but my gut feeling is that would fix it for you. https://webpack.js.org/configuration/dev-server/#devserver-proxy

zakizem commented 6 years ago

Thank you for the quick response vimalloc,

I've seen net internals and all that, I deleted some extensions and I even use private navigation wich I think doesn't work with extensions, still the same problem.

I've set up the webpack proxy and it didn't solve the issue either.

Like I said, if I remove the Jwt Required decorator from my endpoint, the request works perfectly fine.

vimalloc commented 6 years ago

Interesting. Could I get some more details from you to narrow down the issue and see if I can help troubleshoot this further? Ideally I would love a self contained minimal flask app that I could verify this behavior for myself by using the JS console in chrome :+1:

Hopefully if we keep digging we can get to the bottom of this.

zakizem commented 6 years ago

Here are the versions that i'm using :

I tested it without flask restful, it doesn't work most of the time either.

Yes I'm using cookies for the JWTs.

I'm using flask development server.

There are no server logs when the request fails.

I tried two different ways of sending the requet: a simple Javascript xmlHttp request, and the library vue-resource of vue JS.

Here is a little flask app like mine :

from flask import Flask, request, jsonify, make_response
import jwt
import datetime

from flask_restful import Resource, Api
from flask_cors import CORS

app = Flask(__name__)
CORS(app, supports_credentials=True)

api = Api(app)

from flask_jwt_extended import (
    JWTManager, jwt_required, create_access_token,
    jwt_refresh_token_required, create_refresh_token,
    get_jwt_identity, set_access_cookies,
    set_refresh_cookies, unset_jwt_cookies
)
app.config['JWT_TOKEN_LOCATION'] = ['cookies']
app.config['JWT_SESSION_COOKIE'] = True

app.config['JWT_ACCESS_TOKEN_EXPIRES'] = datetime.timedelta(hours=3)

app.config['JWT_ACCESS_COOKIE_PATH'] = '/'
app.config['JWT_REFRESH_COOKIE_PATH'] = '/'

app.config['JWT_COOKIE_CSRF_PROTECT'] = False
app.config['JWT_SECRET_KEY'] = 'clé super secrète man'
jwt = JWTManager(app)

@app.route('/withoutFlaskRestful', methods=['POST'])
@jwt_required
def withoutFlaskRestful():
    return 'hey man !!'

class MyResource(Resource):
    decorators = [jwt_required]
    def post(self):
        return 'hey man !!'

class Authentification(Resource):
    def post(self):
        access_token = create_access_token(identity='bla')
        refresh_token = create_refresh_token(identity='bla')
        resp = jsonify({'login': True,'data': 'bla'})
        set_access_cookies(resp, access_token)
        set_refresh_cookies(resp, refresh_token)
        resp.status_code = 200
        return resp

api.add_resource(MyResource, '/randomURL')
api.add_resource(Authentification, '/authentification')

if __name__ == "__main__":
    print (__name__)
    app.run(host="127.0.0.1", port=5001, threaded=True)
zakizem commented 6 years ago

I think I managed to narrow down the issue, and it's pretty strange:

It's about the port, there are ports where the app works and there is no issue, and in others, the issue is present, and I checked in the windows monitor if the port that I was using was used by some other process, and it isn't the case.

So to summarize:

Do you know the logic of what happens here Vimalloc ? because it seems very strange to me.

Here's an example of a simple flask app, where I made the flask-restful part in comments, that works, for me, 100% on the port 8011 for example, but on the port 5001, it doesn't work all the time:


from flask import Flask, jsonify, request
from flask_cors import CORS
import datetime

# from flask_restful import Resource, Api
from flask_cors import CORS

app = Flask(__name__)
CORS(app, supports_credentials=True)

# api = Api(app)

from flask_jwt_extended import (
    JWTManager, jwt_required, create_access_token,
    jwt_refresh_token_required, create_refresh_token,
    get_jwt_identity, set_access_cookies,
    set_refresh_cookies, unset_jwt_cookies
)
app.config['JWT_TOKEN_LOCATION'] = ['cookies']
app.config['JWT_SESSION_COOKIE'] = True

app.config['JWT_ACCESS_TOKEN_EXPIRES'] = datetime.timedelta(seconds=3)

app.config['JWT_ACCESS_COOKIE_PATH'] = '/'
app.config['JWT_REFRESH_COOKIE_PATH'] = '/'

app.config['JWT_COOKIE_CSRF_PROTECT'] = False
app.config['JWT_SECRET_KEY'] = 'clé super secrète man'
jwt = JWTManager(app)

# ##### API avec flask_restful
#
# class InsertionReponses(Resource):
#     decorators = [jwt_required]
#     def post(self):
#         return 'data'
#
# class Refresh(Resource):
#     decorators = [jwt_refresh_token_required]
#     def get(self):
#         # Create the new access token
#         current_user = get_jwt_identity()
#         access_token = create_access_token(identity=current_user)
#
#         # Set the JWT access cookie in the response
#         resp = jsonify({'refresh': True})
#         set_access_cookies(resp, access_token)
#
#         resp.status_code = 200
#         return resp
#
# api.add_resource(InsertionReponses, '/sendRequest')
# api.add_resource(Refresh, '/token/refresh')
#
# ##### FIN API

@app.route('/simplePostRequest', methods=['POST'])
@jwt_required
def vide():
    data=request.get_json()
    return jsonify({'vide': True,'iii': data})

@app.route('/getTokens', methods=['POST'])
def a():
    access_token = create_access_token(identity='blablue')
    refresh_token = create_refresh_token(identity='blablue')
    resp = jsonify({'login': True,'data': 'bla'})
    set_access_cookies(resp, access_token)
    set_refresh_cookies(resp, refresh_token)
    resp.status_code = 200
    return resp

if __name__ == "__main__":
    print (__name__)
    app.run(host="127.0.0.1", port=8011, threaded=True)
vimalloc commented 6 years ago

That is absolutely bizarre, I have never heard of anything like that before. I tried duplicating this issue and have yet to be able to do so locally, but I haven't had a whole lot of time to really dig into so far.

If it really is the port thing, all I could think of is an anti-virus or firewall or something interfering with the request. I can't think of any reason the port would matter to the client or the flask server. Just curious, what OS are you running this on?

zakizem commented 6 years ago

Yes it's really bizarre... and there isn't just one port that has this problem... I randomly found 3 of them with that behaviour.

For the OS, I'm on Windows 10 64-bit.

mbrucher commented 5 years ago

It seems I have a similar issue with CORS not passing on Chrome. It only appears if I use jwt_required or jwt_optional, I can post something for instance to log in, the cookies are set properly, but Chrome says the OPTIONS is not correct with Access-Control-Allow-Origin not set properly, but there is an access-control-allow-origin header (notice the case difference) with the appropriate values. For the non jwt protected APIs, I don't see access-control-allow-headers and access-control-allow-methods set.