How to validate Keycloak webhook request? - python

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()
...

Related

aiohttp: How to update request headers according to request body?

I am trying to implement a type of custom authentication by using aiohttp something like the example in this link but I also need request body. Here is an example for requests:
class CustomAuth(AuthBase):
def __init__(self, secretkey):
self.secretkey = secretkey
def get_hash(self, request):
if request.body:
data = request.body.decode('utf-8')
else:
data = "{}"
signature = hmac.new(
str.encode(self.secretkey),
msg=str.encode(data),
digestmod=hashlib.sha256
).hexdigest().upper()
return signature
def __call__(self, request):
request.headers["CUSTOM-AUTH"] = self.get_hash(request)
return request
I've looked into tracing and BasicAuth but they are useless in my situation. On on_request_start request body is not ready, on on_request_chunk_sent headers have already been sent. A solution like BasicAuth don't have access the request data at all.
Do you have any idea?
Thanks in advance.

Create a decorator to handle token validation in flask

I have two endpoints defined in my flask service python file.
The first endpoint returns a list of parent and child nodes from a mmap json file which it parses.
The second endpoint returns a specific child field from a mmap json file which it parses.
Each of these endpoints can only be used when the token has been validated. Thus I have the following implementations.
from flask import request
import requests
def check_token(self):
# Method to verify the token from the another python Service
token_header = request.headers['authorization']
# Remove the 'Basic" part from the token
auth_token = token_header.split(maxsplit=1)[1]
self.__payload = {'token' : auth_token}
# Append the token to the header by using the payload
response = requests.get(self.__url, params=self.__payload, verify=False)
return response
# 1. This endpoint returns a list of parent and child nodes from a mmap json file which it parses.
class SystemList(Resource):
def get(self, systemname):
token = check_token()
if token.ok:
# Open the mmap file, parse it, and jsonify the result and return it
# Invalid token present
else:
return make_response(
jsonify("Invalid access as token invalid.")
)
# 2. The endpoint returns a specific child field from a mmap json file which it parses.
class SystemChildList(Resource):
def get(self, systemname, id):
token = check_token()
if token.ok:
# Open the mmap file, parse it, and jsonify the result and return it
# Invalid token present
else:
return make_response(
jsonify("Invalid access as token invalid.")
)
The issue I have is that I want to use a decorator to handle the validation of the token.
I want to be able to add it before the get() method something like the following.
#validatetoken
def get(self, designation):
# I am not sure what goes here and what do I need to put here?
# I want to be able to have one decorator for both of the SystemList and SystemChildList
# resource shown above.
I am not sure on what goes in the decorator. I am really new to these concepts. Any help is appreciated.
You can use method_decorators parameter to achieve this
try,
from flask import request
from functools import wraps
import requests
def check_token(f):
#wraps(f)
def decorated(*args, **kwargs):
token_header = request.headers['authorization']
# Remove the 'Basic" part from the token
auth_token = token_header.split(maxsplit=1)[1]
__url = "url_for_token_validation"
__payload = {'token' : auth_token}
# Append the token to the header by using the payload
response = requests.get(__url, params=__payload, verify=False)
if response.status_code != requests.codes.ok:
return make_response(
jsonify("Invalid access as token invalid.")
)
return f(*args, **kwargs)
return decorated
# 1. This endpoint returns a list of parent and child nodes from a mmap json file which it parses.
class SystemList(Resource):
#check_token
def get(self, systemname):
# Open the mmap file, parse it, and jsonify the result and return it
# 2. The endpoint returns a specific child field from a mmap json file which it parses.
class SystemChildList(Resource):
#check_token
def get(self, systemname, id):
# Open the mmap file, parse it, and jsonify the result and return it

How do I sign the body of a requests.Request in an Auth object's __call__ method?

I'm trying to write a nice auth helper for kraken. I want it to be as automatic as possible, so it needs to:
add a nonce (time.time()*1000) to the POST body
calculate a signature over the POST body
put the signature into the headers
I wrote the obvious code based on this answer:
class KrakenAuth(AuthBase):
"""a requests-module-compatible auth module for kraken.com"""
def __init__(self, key, secret):
self.api_key = key
self.secret_key = secret
def __call__(self, request):
#print("Auth got a %r" % type(request))
nonce = int(1000*time.time())
request.data = getattr(request, 'data', {})
request.data['nonce'] = nonce
request.prepare()
message = request.path_url + hashlib.sha256(str(nonce) + request.body).digest()
hmac_key = base64.b64decode(self.secret_key)
signature = hmac.new(hmac_key, message, hashlib.sha512).digest()
signature = base64.b64encode(signature)
request.headers.update({
'API-Key': self.api_key,
'API-Sign': signature
})
return request
and them I'm calling it (from a wrapper method on another object) like:
def _request(self, method, url, **kwargs):
if not self._auth:
self._auth = KrakenAuth(key, secret)
if 'auth' not in kwargs:
kwargs['auth'] = self._auth
return self._session.request(method, URL + url, **kwargs)
...but it doesn't work. The commented-out print() statement shows that it's getting a PreparedRequest object not a Request object, and thus the call to request.prepare() is a call to PreparedRequest.prepare does nothing useful because there's no request.data because it's already been converted into a body attribute.
You can't access the data attribute of the request, because the authentication object is applied to a requests.PreparedRequest() instance, which has no .data attribute.
The normal flow for a Session.request() call (used by all the request.<method> and session.<method> calls), is as follows:
A Request() instance is created with all the same arguments as the original call
The request is passed to Session.prepare_request(), which merges in session-stored base values with the arguments of the original call first, then
A PreparedRequest() instance is created
The PreparedRequest.prepare() method is called on that prepared request instance, passing in the merged data from the Request instance and the session.
The prepare() method calls the various self.prepare_* methods, including PreparedRequest.prepare_auth().
PreparedRequest.prepare_auth() calls auth(self) to give the authentication object a chance to attach information to the request.
Unfortunately for you, at no point during this flow will the original data mapping be available to anyone else but PreparedRequest.prepare() and PreparedRequest.prepare_body(), and in those methods the mapping is a local variable. You can't access it from the authentication object.
Your options are then:
To decode the body again, and call prepare_body() with the updated mapping.
To not use an authentication object, but use the other path from my answer; to explicitly create a prepared request and manipulate data first.
To play merry hell with the Python stack and extract locals from the prepare() method that is two frames up. I really can't recommend this path.
To keep the authentication method encapsulated nicely, I'd go with decoding / re-encoding; the latter is simple enough by reusing PreparedRequest.prepare_body():
import base64
import hashlib
import hmac
import time
try:
# Python 3
from urllib.parse import parse_qs
except ImportError:
# Python 2
from urlparse import parse_qs
from requests import AuthBase
URL_ENCODED = 'application/x-www-form-urlencoded'
class KrakenAuth(AuthBase):
"""a requests-module-compatible auth module for kraken.com"""
def __init__(self, key, secret):
self.api_key = key
self.secret_key = secret
def __call__(self, request):
ctheader = request.headers.get('Content-Type')
assert (
request.method == 'POST' and (
ctheader == URL_ENCODED or
requests.headers.get('Content-Length') == '0'
)
), "Must be a POST request using form data, or empty"
# insert the nonce in the encoded body
data = parse_qs(request.body)
data['nonce'] = nonce
request.prepare_body(data, None, None)
body = request.body
if not isinstance(body, bytes): # Python 3
body = body.encode('latin1') # standard encoding for HTTP
message = request.path_url + hashlib.sha256(b'%s%s' % (nonce, body)).digest()
hmac_key = base64.b64decode(self.secret_key)
signature = hmac.new(hmac_key, message, hashlib.sha512).digest()
signature = base64.b64encode(signature)
request.headers.update({
'API-Key': self.api_key,
'API-Sign': signature
})
return request

How do I dynamically decorate a Flask rule?

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.

Create SoapRequest without sending them with Suds/Python

Is there anyway to get suds returning the SoapRequest (in XML) without sending it?
The idea is that the upper levels of my program can call my API with an additional boolean argument (simulation).
If simulation == false then process the other params and send the request via suds
If simulation == false then process the other params, create the XML using suds (or any other way) and return it to the caller without sending it to the host.
I already implemented a MessagePlugin follwing https://fedorahosted.org/suds/wiki/Documentation#MessagePlugin, but I am not able to get the XML, stop the request and send back the XML to the caller...
Regards
suds uses a "transport" class called HttpAuthenticated by default. That is where the actual send occurs. So theoretically you could try subclassing that:
from suds.client import Client
from suds.transport import Reply
from suds.transport.https import HttpAuthenticated
class HttpAuthenticatedWithSimulation(HttpAuthenticated):
def send(self, request):
is_simulation = request.headers.pop('simulation', False)
if is_simulation:
# don't actually send the SOAP request, just return its XML
return Reply(200, request.headers.dict, request.msg)
return HttpAuthenticated(request)
...
sim_transport = HttpAuthenticatedWithSimulation()
client = Client(url, transport=sim_transport,
headers={'simulation': is_simulation})
It's a little hacky. (For example, this relies on HTTP headers to pass the boolean simulation option down to the transport level.) But I hope this illustrates the idea.
The solution that I implemented is:
class CustomTransportClass(HttpTransport):
def __init__(self, *args, **kwargs):
HttpTransport.__init__(self, *args, **kwargs)
self.opener = MutualSSLHandler() # I use a special opener to enable a mutual SSL authentication
def send(self,request):
print "===================== 1-* request is going ===================="
is_simulation = request.headers['simulation']
if is_simulation == "true":
# don't actually send the SOAP request, just return its XML
print "This is a simulation :"
print request.message
return Reply(200, request.headers, request.message )
return HttpTransport.send(self,request)
sim_transport = CustomTransportClass()
client = Client(url, transport=sim_transport,
headers={'simulation': is_simulation})
Thanks for your help,

Categories