This library provides an extension for protecting APIs with OAuth when using Flask.
You can either install this module with pip:
pip install -U flask-of-oil
Or copy the flask_of_oil folder in your project and install the requirements in requirements.txt
using pip or pipenv
This filter can be used in two ways with Flask, either to run before all routes with the same authorization requirement,
which protects all endpoints. Many times however it's desirable to only protect certain endpoints, which then can be done
using the decorator
pattern described below.
When running the filter before_request all routes, the same configuration will apply to all routes. So if the filter is configured to require a scope of "read" then all routes will require that scope. If routes have different needs then the decorator pattern should be used (see next section).
Example using before_request
import json
from flask import g, Flask
from flask_of_oil.oauth_filter import OAuthFilter
_app = Flask(__name__)
_oauth = OAuthFilter(verify_ssl=True)
_app.before_request(_oauth.filter)
@_app.route('/hello_world')
def hello_world():
"""
:return: Returns a very useful JSON message when accessed.
"""
print("OAuth Access token used for access")
return json.dumps({"hello": g.user})
Instead of setting the before_request
a decorator can be added to the route that should be protected. This also enables the routes to have
different scope requirements which could be handy.
Important: The oauth decorator needs to be CLOSEST to the function
import json
from flask import g, Flask
from flask_of_oil.oauth_filter import OAuthFilter
_app = Flask(__name__)
_oauth = OAuthFilter(verify_ssl=True)
@_app.route('/hello_world')
@_oauth.protect(["read"])
def hello_world():
"""
:return: Returns a very useful JSON message when accessed.
"""
print("OAuth Access token used for access")
return json.dumps({"hello": g.user})
The scope parameter of the protect decorator must either be a list or a space separated string:
["scope1", "scope2]
or
"scope1 scope2"
The incoming request can also be authorized based on claims, or a combination of claims and scopes.
The claims parameter of the protect decorator method has to be a dict
, with keys the claims that are required
for the request to be allowed. The value None
instructs the filter to not check the value for the specific claim.
# Only allow requests where the incoming access token has the scope read and it contains a claim named MyGoodClaim
@_oauth.protect(scopes=["read"], claims={"MyGoodClaim": None})
# Only allow requests where the incoming access token has the scope write and it contains a claim named MyGoodClaim with value MyGoodValue
@_oauth.protect(scopes=["write"], claims={"MyGoodClaim": "MyGoodValue"})
Filter global variable
The OAuth filter should be setup the same way as Flask, a global reference and then initialized in main (or with the application) The initialization depends on the type of tokens received. See the following examples.
from flask import g, Flask
from flask_of_oil.oauth_filter import OAuthFilter
_app = Flask(__name__)
_oauth = OAuthFilter(verify_ssl=True)
Using Opaque tokens
When using Opaque tokens, the filter needs to resolve the reference by calling the introspection endpoint of the OAuth server, this endpoint requires client credentials so the API needs to be a client of the OAuth server with the permission to introspect tokens.
if __name__ == '__main__':
# configure the oauth filter
_oauth.configure_with_opaque("https://oauth-server-host/oauth/v2/introspection", "api-client-id", "api-client-secret")
# initiate the Flask app
_app.run("localhost", debug=True, port=8000,
ssl_context="adhoc")
Using JWT tokens
When using JWT (JWS) tokens, the filter will validate the signature of the token with the key that is provided on the JWKS (Json Web Key Service) endpoint. The JWT contains a key id (kid) that is matched against the available public keys on the OAuth server and then validated with that key.
if __name__ == '__main__':
# configure the oauth filter
_oauth.configure_with_jwt("https://oauth-server-host/oauth/v2/metadata/jwks", "configured-issuer", "audience-of-token")
# initiate the Flask app
_app.run("localhost", debug=True, port=8000,
ssl_context="adhoc")
Using JWTs with multiple issuers
You can configure multiple issuers of your tokens. The validator will match the issuer from the incoming JWT with one from the configured list to properly validate the token.
if __name__ == '__main__':
# configure the oauth filter
_oauth.configure_with_multiple_jwt_issuers(["https://idsvr.example.com/configured-issuer", "https://idsvr.example.com/another-issuer"], "audience-of-token")
# initiate the Flask app
_app.run("localhost", debug=True, port=8000,
ssl_context="adhoc")
When the filter accepts the request, it sets the request.claims
context local variable for that request with all
the token claims. For JWT tokens, this is the JWT payload and for opaque tokens the introspection response.
For example, the subject of the Authorization can be accessed like so request.claims["sub"]
.
The filter may abort the request if the Access token is invalid or if the scopes or claims in the access token doesn't match the required scopes or claims for the route.
401 Unauthorized
When an invalid token is presented the filter will give a 401 unauthorized. To customize the response, use Flasks errorhandler to add a response.
@_app.errorhandler(401)
def unauthorized(error):
return json.dumps({'error': "unauthorized",
"error_description": "No valid access token found"}), \
401, {'Content-Type': 'application/json; charset=utf-8'}
403 Forbidden
When a valid token is presented the filter but it's missing the appropriate scopes or claims (or the claims have wrong values) then the request is aborted with a 403 Forbidden.
@_app.errorhandler(403)
def forbidden(error):
return json.dumps({'error': "forbidden",
"error_description": "Access token is missing appropriate scopes"}), \
403, {'Content-Type': 'application/json; charset=utf-8'}
You can run the test suite with the following command:
pytest --log-level=DEBUG
If you want to run tests from a concrete file, use this command:
pytest --log-level=DEBUG tests/test_jwt_validator.py
It's best to run the tests in a dedicated Python virtual environment. Follow these steps to create the environment, install all dependencies, then run the test suite:
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
pytest --log-level=DEBUG tests/test_jwt_validator.py
When you're done with the tests, run:
deactivate
to exit the virtual environment.
For questions and support, contact Curity AB:
Curity AB
Copyright (C) 2016 Curity AB.