I have a application with a large number of automatically defined rules. I need every rule to check for a Bearer token before proceeding.
I have found app.url_map.iter_rules() which does let me see all the defined rules. I want a way to redefine every rule to use its current function, but wrap it with the authenticate_admin decorator defined below.
from flask import Flask
from functools import wraps
app = Flask(__name__)
# Check that the request has the correct `bearer_token`.
def authenticate_admin(func):
#wraps(func)
def wrapped(*args, **kwargs):
bearer_token = vault.get('secret/oauth')['bearer_token']
expected = ("Bearer " + bearer_token)
if expected != request.headers.get('Authorization'):
return jsonify({'error': "Authorization token incorrect"}), 401
return func(*args, **kwargs)
return wrapped
# .... Define a bunch of routes (Elided) ....
for rule in app.url_map.iter_rules():
# NEXT LINE IS PSEUDOCODE; IT IS WHAT I WANT TO ACHIEVE
rule.fx = authenticate_admin(rule.fx)
What I want is that after the above for loop executes, all rules will now require you to supply a Bearer token.
In the end, I just ended up adding following davidism's advice and made authenticate a function that is run by all endpoints. I added a whitelist of endpoints that do NOT require authentication.
from flask import request
import vault
app = Flask(__name__)
# .... Define a bunch of routes (Elided) ....
WHITELIST_POST = ['/post1', '/post2', '/post3']
WHITELIST_GET = ['/', '/get1', '/get2']
def authenticate():
if request.method == "GET" and request.url_rule.rule in WHITELIST_GET:
return
if request.method == "POST" and request.url_rule.rule in WHITELIST_POST:
return
bearer_token = vault.get('secret/oauth')['bearer_token']
expected = ("Bearer " + bearer_token)
if expected != request.headers.get('Authorization'):
abort(401)
app.before_request(authenticate)
On a side note, my boss liked this even more. It means that any endpoint we create on the server by default requires a bearer token. In the past we have forgotten to require a bearer token for some endpoints. Now it is opt out instead of opt in.
Related
I'm implementing an endpoint to receive events from Keycloak using a webhook, but I don't know how to validate this request.
I see that the request contains a header "X-Keycloak-Signature". Also, I set a WEBHOOK_SECRET. It seems I somehow need to generate this signature from the request and the secret and then compare them. So it looks like this:
import os
import hashlib
from flask import abort, request
def validate_keycloak_signature(f):
def wrapper(self, *args, **kwargs):
secret = os.getenv("WEBHOOK_SECRET")
method = request.method
uri = request.url
body = request.get_data(as_text=True)
smub = secret + method + uri + body
h = hashlib.sha256(smub.encode("utf-8")).hexdigest()
signature = request.headers.get("X-Keycloak-Signature")
if h != signature:
return abort(403)
return f(self, *args, **kwargs)
return wrapper
However, I don't know the algorithm. Here, I tried this one:
1. Create a string that concatenates together the following: Client secret + http method + URI + request body (if present)
2. Create a SHA-256 hash of the resulting string.
3. Compare the hash value to the signature. If they're equal then this request has passed validation.
But it doesn't work. Does anybody has any ideas?
I realized that this signature is generated not by Keycloak itself, but by Phasetwo which provides a webhook plugin for me.
So that I just looked into its code and found out the algorithm.
That is how to generate signature to validate it:
def validate_keycloak_signature(f):
...
secret = os.getenv("WEBHOOK_SECRET")
body = request.data
control_signature = hmac.new(
key=bytes(secret, "utf-8"),
msg=body,
digestmod=hashlib.sha256
).hexdigest()
...
I'm attempting to set up the Azure AD authentication example from here:
https://github.com/Azure-Samples/ms-identity-python-webapp
I have all configurations set, and permissions on AzureAD, but I'm getting intermittent problems with being able to log in.
I have chased the problem to the session keys either not writing or reading correctly within session.get('user'):
def index():
if not session.get("user"):
return redirect(url_for("login"))
return render_template('index.html', user=session["user"], version=msal.__version__)
additionally,
if request.args.get('state') != session.get("state"):
return redirect(url_for("index")) # No-OP. Goes back to Index page
never returns true, but if I comment it out then sometimes all will work, it will log in, and allow access to the graph app.
I've set the flask_session directory locally, and it is creating caches (around 8kb) for each login.
What seems to help is clearing out the flask_session folder and restarting the application, but not reliably.
Any help would be most gratefully recieved!
full example copied here for convenience:
import uuid
import requests
from flask import Flask, render_template, session, request, redirect, url_for
from flask_session import Session # https://pythonhosted.org/Flask-Session
import msal
import app_config
app = Flask(__name__)
app.config.from_object(app_config)
Session(app)
# This section is needed for url_for("foo", _external=True) to automatically
# generate http scheme when this sample is running on localhost,
# and to generate https scheme when it is deployed behind reversed proxy.
# See also https://flask.palletsprojects.com/en/1.0.x/deploying/wsgi-standalone/#proxy-setups
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
#app.route("/")
def index():
if not session.get("user"):
return redirect(url_for("login"))
return render_template('index.html', user=session["user"], version=msal.__version__)
#app.route("/login")
def login():
session["state"] = str(uuid.uuid4())
# Technically we could use empty list [] as scopes to do just sign in,
# here we choose to also collect end user consent upfront
auth_url = _build_auth_url(scopes=app_config.SCOPE, state=session["state"])
return render_template("login.html", auth_url=auth_url, version=msal.__version__)
#app.route(app_config.REDIRECT_PATH) # Its absolute URL must match your app's redirect_uri set in AAD
def authorized():
if request.args.get('state') != session.get("state"):
return redirect(url_for("index")) # No-OP. Goes back to Index page
if "error" in request.args: # Authentication/Authorization failure
return render_template("auth_error.html", result=request.args)
if request.args.get('code'):
cache = _load_cache()
result = _build_msal_app(cache=cache).acquire_token_by_authorization_code(
request.args['code'],
scopes=app_config.SCOPE, # Misspelled scope would cause an HTTP 400 error here
redirect_uri=url_for("authorized", _external=True))
if "error" in result:
return render_template("auth_error.html", result=result)
session["user"] = result.get("id_token_claims")
_save_cache(cache)
return redirect(url_for("index"))
#app.route("/logout")
def logout():
session.clear() # Wipe out user and its token cache from session
return redirect( # Also logout from your tenant's web session
app_config.AUTHORITY + "/oauth2/v2.0/logout" +
"?post_logout_redirect_uri=" + url_for("index", _external=True))
#app.route("/graphcall")
def graphcall():
token = _get_token_from_cache(app_config.SCOPE)
if not token:
return redirect(url_for("login"))
graph_data = requests.get( # Use token to call downstream service
app_config.ENDPOINT,
headers={'Authorization': 'Bearer ' + token['access_token']},
).json()
return render_template('display.html', result=graph_data)
def _load_cache():
cache = msal.SerializableTokenCache()
if session.get("token_cache"):
cache.deserialize(session["token_cache"])
return cache
def _save_cache(cache):
if cache.has_state_changed:
session["token_cache"] = cache.serialize()
def _build_msal_app(cache=None, authority=None):
return msal.ConfidentialClientApplication(
app_config.CLIENT_ID, authority=authority or app_config.AUTHORITY,
client_credential=app_config.CLIENT_SECRET, token_cache=cache)
def _build_auth_url(authority=None, scopes=None, state=None):
return _build_msal_app(authority=authority).get_authorization_request_url(
scopes or [],
state=state or str(uuid.uuid4()),
redirect_uri=url_for("authorized", _external=True))
def _get_token_from_cache(scope=None):
cache = _load_cache() # This web app maintains one cache per session
cca = _build_msal_app(cache=cache)
accounts = cca.get_accounts()
if accounts: # So all account(s) belong to the current signed-in user
result = cca.acquire_token_silent(scope, account=accounts[0])
_save_cache(cache)
return result
app.jinja_env.globals.update(_build_auth_url=_build_auth_url) # Used in template
if __name__ == "__main__":
app.run()
I am one of the maintainer of the Azure AD authentication example from here: https://github.com/Azure-Samples/ms-identity-python-webapp
and I am the author of my workaround repo https://github.com/rayluo/flask-session
As of this writing, an indirect upstream module, cachelib, has released a bugfix for this 2 days ago, at about the same time you encountered this issue. So, to any future readers here: you should not have to explicitly switch to use the aforementioned workaround. That Azure AD web app sample will properly declare the right dependencies it needs.
I found the solution, or more accuratly my failing:
This repo:
https://github.com/rayluo/flask-session
Is not the same as this one:
https://github.com/fengsp/flask-session
The included requirements.txt loads the newer/maintained flask-session from rayluo (thanks!) rather than the one from pip.
So for anyone else finding this, make sure you use the right version!
I would like to route to a different Flask views based on the Accept HTTP header, for example:
#api.route('/test', accept='text/html')
def test_html():
return "<html><body>Test</body></html>"
#api.route('/test', accept='text/json')
def test_json():
return jsonify(test="Test")
I haven't found relevant option in Werkzeug Rule constructor, which is used by Flask. Is it a missing feature or is it possible to achieve the same effect differently, for example by intercepting and modifying URL path before routing?
I don't want to merge the views into one because it would complicate code significantly, there are many of them and they reside in different blueprints.
I am aware that similar question has been asked, but nobody answered it using Flask. It's possible to do it in different web frameworks, for example in Pyramid using predicates - sample code can be found in this answer.
I know this is an old question but I ended up here looking for something similar so I hope it helps someone else.
flask_accept has the functionality to handle different Accept types through different routes.
from flask import Flask, jsonify
from flask_accept import accept
app = Flask(__name__)
#app.route('/')
#accept('text/html')
def hello_world():
return 'Hello World!'
#hello_world.support('application/json')
def hello_world_json():
return jsonify(result="Hello World!")
if __name__ == '__main__':
app.run()
if you just want to reject requests depending on whether they are a specific data type you could also use Flask-Negotiate
from flask import Flask
from flask_negotiate import consumes, produces
app = Flask(__name__)
#app.route('/consumes_json_only')
#consumes('application/json')
def consumes_json_only():
return 'consumes json only'
When one tries to access the endpoint without a valid Accept header:
$ curl localhost:5000 -I
HTTP 415 (Unsupported Media Type)
I wrote a decorator which does that (copying here for posterity). It's just a rough idea that could be improved further (e.g. returning 406 Not Acceptable response instead of using the default handler when there are no handlers that match given MIME type). More explanations are in the comments.
import functools
from flask import Flask, request, jsonify
app = Flask(__name__)
def accept(func_or_mimetype=None):
"""Decorator which allows to use multiple MIME type handlers for a single
endpoint.
"""
# Default MIME type.
mimetype = 'text/html'
class Accept(object):
def __init__(self, func):
self.default_mimetype = mimetype
self.accept_handlers = {mimetype: func}
functools.update_wrapper(self, func)
def __call__(self, *args, **kwargs):
default = self.default_mimetype
mimetypes = request.accept_mimetypes
best = mimetypes.best_match(self.accept_handlers.keys(), default)
# In case of Accept: */*, choose default handler.
if best != default and mimetypes[best] == mimetypes[default]:
best = default
return self.accept_handlers[best](*args, **kwargs)
def accept(self, mimetype):
"""Register a MIME type handler."""
def decorator(func):
self.accept_handlers[mimetype] = func
return func
return decorator
# If decorator is called without argument list, return Accept instance.
if callable(func_or_mimetype):
return Accept(func_or_mimetype)
# Otherwise set new MIME type (if provided) and let Accept act as a
# decorator.
if func_or_mimetype is not None:
mimetype = func_or_mimetype
return Accept
#app.route('/')
#accept # Or: #accept('text/html')
def index():
return '<strong>foobar</strong>'
#index.accept('application/json')
def index_json():
return jsonify(foobar=True)
#index.accept('text/plain')
def index_text():
return 'foobar\n', 200, {'Content-Type': 'text/plain'}
You can return different response types based on the Accept header using request. Example.
if request.accept_mimetypes['application/json']:
return jsonify(<object>), '200 OK'
I am currently trying to write some unit tests for my Flask application. In many of my view functions (such as my login), I redirect to a new page. So for example:
#user.route('/login', methods=['GET', 'POST'])
def login():
....
return redirect(url_for('splash.dashboard'))
I'm trying to verify that this redirect happens in my unit tests. Right now, I have:
def test_register(self):
rv = self.create_user('John','Smith','John.Smith#myschool.edu', 'helloworld')
self.assertEquals(rv.status, "200 OK")
# self.assert_redirects(rv, url_for('splash.dashboard'))
This function does make sure that the returned response is 200, but the last line is obviously not valid syntax. How can I assert this? My create_user function is simply:
def create_user(self, firstname, lastname, email, password):
return self.app.post('/user/register', data=dict(
firstname=firstname,
lastname=lastname,
email=email,
password=password
), follow_redirects=True)
Flask has built-in testing hooks and a test client, which works great for functional stuff like this.
from flask import url_for, request
import yourapp
test_client = yourapp.app.test_client()
with test_client:
response = test_client.get(url_for('whatever.url'), follow_redirects=True)
# check that the path changed
assert request.path == url_for('redirected.url')
For older versions of Flask/Werkzeug the request may be available on the response:
from flask import url_for
import yourapp
test_client = yourapp.app.test_client()
response = test_client.get(url_for('whatever.url'), follow_redirects=True)
# check that the path changed
assert response.request.path == url_for('redirected.url')
The docs have more information on how to do this, although FYI if you see "flaskr", that's the name of the test class and not anything in Flask, which confused me the first time I saw it.
Try Flask-Testing
there is api for assertRedirects you can use this
assertRedirects(response, location)
Checks if response is an HTTP redirect to the given location.
Parameters:
response – Flask response
location – relative URL (i.e. without http://localhost)
TEST script:
def test_register(self):
rv = self.create_user('John','Smith','John.Smith#myschool.edu', 'helloworld')
assertRedirects(rv, url of splash.dashboard)
One way is to not follow the redirects (either remove follow_redirects from your request, or explicitly set it to False).
Then, you can simply replace self.assertEquals(rv.status, "200 OK") with:
self.assertEqual(rv.status_code, 302)
self.assertEqual(rv.location, url_for('splash.dashboard', _external=True))
If you want to continue using follow_redirects for some reason, another (slightly brittle) way is to check for some expected dashboard string, like an HTML element ID in the response of rv.data. e.g. self.assertIn('dashboard-id', rv.data)
You can verify the final path after redirects by using Flask test client as a context manager (using the with keyword). It allows keeping the final request context around in order to import the request object containing request path.
from flask import request, url_for
def test_register(self):
with self.app.test_client() as client:
user_data = dict(
firstname='John',
lastname='Smith',
email='John.Smith#myschool.edu',
password='helloworld'
)
res = client.post('/user/register', data=user_data, follow_redirects=True)
assert res.status == '200 OK'
assert request.path == url_for('splash.dashboard')
I have the following Flask routes and a custom helper:
from spots import app, db
from flask import Response
import simplejson as json
def json_response(action_func):
def create_json_response(*args, **kwargs):
ret = action_func(*args, **kwargs)
code = 200
if len(ret) == 2:
code = ret[0]
resp = ret[1]
else:
resp = ret[0]
return Response(
response=json.dumps(resp, indent=4),
status=code,
content_type='application/json'
)
return create_json_response
#app.route('/test')
#json_response
def test():
return 400, dict(result="Test success")
#app.route('/')
#json_response
def home():
return 200, dict(result="Home success")
I would expect a GET request to /test to return something like {"result":"Test success"} but that is not the case. Instead, any request seems to match the last route, i.e. home. Why?
I wonder if this is caused by some lack of insulation between the different calls to json_response?
Thanks in advance.
As Видул Петров said the solution is to use functools.wraps:
import functools
def json_response(action_func):
#functools.wraps(action_func)
def create_json_response(*args, **kwargs):
...
return create_json_response
The reason is that Flask’s routing system maps URLs to "endpoints", and then endpoints to view functions. The endpoint defaults to the __name__ attribute of the view function. In this case the decorated function was passed to app.route so the endpoint was create_json_response for both rules and the last view defined for that endpoint was used in both cases.
functools.wraps takes the __name__ (and other attributes) from the original function and fixes this. It is always a good idea to use it in decorated wrappers.