corydolphin / flask-cors

Cross Origin Resource Sharing ( CORS ) support for Flask
https://flask-cors.readthedocs.io/en/latest/index.html
MIT License
889 stars 140 forks source link

Using flask-cors with flask-restful and @before_request decorator for jwt auth #201

Closed gwvt closed 7 years ago

gwvt commented 7 years ago

I'm trying to use flask-cors for the development configuration for a flask-restful api, simplified below:

import config

from flask import Flask, request
from flask_restful import Api, Resource
from flask_cors import CORS

app = Flask(__name__)
app.config.from_object('config.DevelopmentConfig')
api = Api(app)
if app.config['CORS_ENABLED'] is True:
    CORS(app, origins="http://127.0.0.1:8080", allow_headers=[
        "Content-Type", "Authorization", "Access-Control-Allow-Credentials"],
        supports_credentials=True)

@app.before_request
def authorize_token():
    if request.endpoint != 'token':
        try:
            authorize_jwt(request)
        except Exception as e:
            return "401 Unauthorized\n{}\n\n".format(e), 401

class GetToken(Resource):
    def post(self):
        token = generate_jwt()
        return token       # token sent to client to return in subsequent requests in Authorization header

# requires authentication through before_request decorator
class Test(Resource):
    def get(self):
        return {"test": "testing"}

api.add_resource(GetToken, '/token', endpoint='token')
api.add_resource(Test, '/test', endpoint='test')

if __name__ == '__main__':
    app.run()

But whatever I try I always get the error 'Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.'

Without the JWT auth piece, everything else works fine. (And the JWT auth works fine without flask-cors.) Seems like the hangup is something with using flask-cors with the before_request decorator (?).

Any suggestions?

gwvt commented 7 years ago

Any help with this would be much appreciated. Thanks!

corydolphin commented 7 years ago

Hey @gwvt can you post the (non secret) details of the config file? If I have a fully working example, I will look at this tonight.

Sorry for the delay!

gwvt commented 7 years ago

Great, thanks so much! Here's the simplified config file:

class Config(object):
    DEBUG = False
    TESTING = False
    HOST_NAME = 'localhost:5000'
    DATABASE_URI = 'postgresql+psycopg2://localhost:5432/database'
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    CORS_ENABLED = False

class DevelopmentConfig(Config):
    DEBUG = True
    CORS_ENABLED = True

Let me know if you need any other info.

gwvt commented 7 years ago

Or here's a complete self-contained application:

from flask import Flask, request
from flask_restful import Api, Resource
from flask_cors import CORS

app = Flask(__name__)
api = Api(app)
CORS(app, origins="http://127.0.0.1:8080", allow_headers=[
    "Content-Type", "Authorization", "Access-Control-Allow-Credentials"],
    supports_credentials=True)

@app.before_request
def authorize_token():
    if request.endpoint != 'token':
        try:
            auth_header = request.headers.get("Authorization") 
            if "Bearer" in auth_header:
                token = auth_header.split(' ')[1]
                if token != '12345678':
                    raise ValueError('Authorization failed.')
        except Exception as e:
            return "401 Unauthorized\n{}\n\n".format(e), 401

class GetToken(Resource):
    def post(self):
        token = '12345678'
        return token       # token sent to client to return in subsequent
        # requests in Authorization header

# requires authentication through before_request decorator
class Test(Resource):
    def get(self):
        return {"test": "testing"}

api.add_resource(GetToken, '/token', endpoint='token')
api.add_resource(Test, '/test', endpoint='test')

if __name__ == '__main__':
    app.run()

Same thing, getting 'Response to preflight request doesn't pass access control check' error message when the api is called from localhost:8080, sending a request to endpoint '/test' with header 'Authorization: Bearer 12345678'.

corydolphin commented 7 years ago

Sorry for the delay, I finally got a chance to look at this properly.

It looks like the issue is with your origin header. What is the 'Origin' you are sending the CORS request from? Your configuration will only allow browsers to issue a CORS request from "http://127.0.0.1:8080" to your flask-cors app (running by default on localhost:5000).

Example: If a browser were currently looking at an html page on "http://127.0.0.1:8080", and the JS on that page issued an XHR to e.g. localhost:5050/token, they would send a request like this, and receive a similarly succesful response.

➜  flask-cors-test curl --include -X GET http://127.0.0.1:5000/test --header Origin:http://127.0.0.1:8080 --header 'Authorization: Bearer 12345678'
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 20
Access-Control-Allow-Origin: http://127.0.0.1:8080
Access-Control-Allow-Credentials: true
Server: Werkzeug/0.12.1 Python/2.7.13
Date: Sat, 22 Apr 2017 20:47:26 GMT

{"test": "testing"}

But, if instead your browser is pointing to something even slightly different, e.g. localhost:8080 (which would resolve to the same thing on your machine), the browser will see the issue you are reporting. The browser will issue a command similar to this, and receive a non-cors response:

➜  flask-cors-test curl --include -X GET http://127.0.0.1:5000/test --header Origin:http://localhost:8080 --header 'Authorization: Bearer 12345678'
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 20
Server: Werkzeug/0.12.1 Python/2.7.13
Date: Sat, 22 Apr 2017 20:49:33 GMT

{"test": "testing"}

Does that make sense? Sorry for the delay again!

gwvt commented 7 years ago

Thank you very much for your reply. That's my mistake in the code specifying host of origin, but that actually isn't the issue. With curl sending a normal GET request, everything works, but the issue is with the preflight request sent by the browser via the OPTIONS method with headers. I tried both with Flask-Restful as well as a standard Flask application, with the same results (see below for code).

The endpoints without the before_request decorator that checks the JWT token work as expected, but sending a request to the '/test' endpoint with the before_request decorator returns this error message in Chrome:

XMLHttpRequest cannot load http://127.0.0.1:5000/test. Response for preflight has invalid HTTP status code 401

The response body is:

401 Unauthorized
argument of type 'NoneType' is not iterable

The headers for the request and response are:

General:
Request URL:http://127.0.0.1:5000/test
Request Method:OPTIONS
Status Code:401 UNAUTHORIZED
Remote Address:127.0.0.1:5000
Referrer Policy:no-referrer-when-downgrade
Response Headers
view source

Response Headers:
Access-Control-Allow-Credentials:true
Access-Control-Allow-Headers:authorization
Access-Control-Allow-Methods:DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT
Access-Control-Allow-Origin:http://localhost:8080
Content-Length:62
Content-Type:text/html; charset=utf-8
Date:Mon, 24 Apr 2017 14:51:22 GMT
Server:Werkzeug/0.12.1 Python/2.7.10
Request Headers
view source

Request Headers:
Accept:*/*
Accept-Encoding:gzip, deflate, sdch, br
Accept-Language:en-US,en;q=0.8
Access-Control-Request-Headers:authorization
Access-Control-Request-Method:GET
Connection:keep-alive
DNT:1
Host:127.0.0.1:5000
Origin:http://localhost:8080
Referer:http://localhost:8080/
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36
Name

Is there something I'm missing is setting up CORS with the before_request decorator? Any guidance will be very much appreciated. Thank you!

*

Code for self-contained application, Flask-Restful:

from flask import Flask, request
from flask_restful import Api, Resource
from flask_cors import CORS

app = Flask(__name__)
api = Api(app)
CORS(app, origins="http://localhost:8080", allow_headers=[
    "Content-Type", "Authorization", "Access-Control-Allow-Credentials"],
    supports_credentials=True, intercept_exceptions=False)

@app.before_request
def authorize_token():
    if request.endpoint == 'test':
        try:
            auth_header = request.headers.get("Authorization")
            if "Bearer" in auth_header:
                token = auth_header.split(' ')[1]
                if token != '12345678':
                    raise ValueError('Authorization failed.')
        except Exception as e:
            return "401 Unauthorized\n{}\n\n".format(e), 401

class GetToken(Resource):
    def post(self):
        token = '12345678'
        return token

# requires authentication through before_request decorator
class Test(Resource):
    def get(self):
        return {"test": "testing"}

class TestHeaders(Resource):
    def get(self):
        auth_header = request.headers.get("Authorization")
        token = auth_header.split(' ')[1]
        return token

api.add_resource(GetToken, '/token', endpoint='token')
api.add_resource(Test, '/test', endpoint='test')
api.add_resource(TestHeaders, '/headers', endpoint='headers')

if __name__ == '__main__':
    app.run(debug=True)

And the equivalent plain Flask application:

from flask import Flask, request
from flask_cors import CORS

app = Flask(__name__)
CORS(app, origins="http://localhost:8080", allow_headers=[
    "Content-Type", "Authorization", "Access-Control-Allow-Credentials"],
    supports_credentials=True, intercept_exceptions=False)

@app.before_request
def authorize_token():
    if request.endpoint == 'test':
        try:
            auth_header = request.headers.get("Authorization")
            if "Bearer" in auth_header:
                token = auth_header.split(' ')[1]
                if token != '12345678':
                    raise ValueError('Authorization failed.')
        except Exception as e:
            return "401 Unauthorized\n{}\n\n".format(e), 401

@app.route('/token', methods=['POST'])
def get_token():
    token = '12345678'
    return token

# requires authentication through before_request decorator
@app.route('/test', methods=['GET'])
def test():
        return 'test'

@app.route('/headers', methods=['GET'])
def test_headers():
    auth_header = request.headers.get("Authorization")
    token = auth_header.split(' ')[1]
    return token

if __name__ == '__main__':
    app.run(debug=True)
corydolphin commented 7 years ago

This looks to be an application issue, auth_header is None, so: if "Bearer" in auth_header: is failing.

gwvt commented 7 years ago

Right. The test_headers() function assigns the value of the Authorization header to auth_header, while the test() function that is 'protected' by the before_request decorator and authorize_token function does not.

I figured out that the problem was that the authorize_token function requires a test to run the function only on the passed GET method, not the preflight request, so this now works:

@app.before_request
def authorize_token():
    if request.endpoint == 'test':
        try:
            if request.method != 'OPTIONS':  # <-- required
                auth_header = request.headers.get("Authorization")
                if "Bearer" in auth_header:
                    token = auth_header.split(' ')[1]
                    if token != '12345678':
                        raise ValueError('Authorization failed.')
        except Exception as e:
            return "401 Unauthorized\n{}\n\n".format(e), 401
xiaoming401 commented 6 years ago

I've encountered same problem and this page helps a lot.Thx!

gemisolocnv commented 2 years ago

You are my savior @gwvt . Thank you very much

petatemarvin26 commented 2 years ago

for what was the purpose of request.method == 'OPTIONS' in Flask Middleware, before...request and after...request, 🤔 because it fires up 2 times, the first fire up has method of OPTIONS and the second one is the <DEFAULT METHOD> you've used 🤔