Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
gwvt opened this issue Apr 10, 2017 · 11 comments
Closed

Comments

@gwvt
Copy link

gwvt commented Apr 10, 2017

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
Copy link
Author

gwvt commented Apr 17, 2017

Any help with this would be much appreciated. Thanks!

@corydolphin
Copy link
Owner

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
Copy link
Author

gwvt commented Apr 17, 2017

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
Copy link
Author

gwvt commented Apr 18, 2017

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
Copy link
Owner

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
Copy link
Author

gwvt commented Apr 24, 2017

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
Copy link
Owner

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

@gwvt
Copy link
Author

gwvt commented Apr 25, 2017

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
Copy link

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

@gemisolocnv
Copy link

You are my savior @gwvt . Thank you very much

@petatemarvin26
Copy link

petatemarvin26 commented Mar 19, 2022

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 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants