Verifying AzureAD access token with Django-REST-framework - python

Before anything I would like to warn you about my extremely limited knowledge on the subject.
Now that you've been warned, what I need to ask is how can I verify and azureAD access token with Django/django-rest-framework.
I have a app that I need to sign in to with azureAD, which means I need to get an access token from azureAD, and thankfully this is will be done on the frontend side with Angular what I need to do is verify that token on the backend side with django/django-rest-framework and I have no idea where to start from, can anyone suggest a way to do this or send me in the right direction ?
Thank you very much.

Ok, so I owe this to my past self.
What I did is got the authorization from the headers of the request, sliced it so it's the token only and used the used the microsoft keys url, audience and jwt package to decode it, here is the code:
url = "https://login.microsoftonline.com/5c9b264f-33ad-4093-bb65-8d14aaec9f63/discovery/v2.0/keys"
valid_audience = 'api://0d44e6da-8e5b-4e98-94b5-5f02ce228647'
response = urlopen(url)
jwks = json.loads(response.read())
def is_logged_in(func):
def wrapper(self, *args, **kwargs):
token = self.headers['Authorization'][7:] # Need to test this against the frontend request.
try:
jwks_client = PyJWKClient(url)
signing_key = jwks_client.get_signing_key_from_jwt(token)
decoded = jwt.decode(token,
signing_key.key,
algorithms=["RS256"],
audience=valid_audience)
return func(self, *args, **kwargs)
except Exception as e:
print(e)
return HttpResponse('Unauthorized', status=401)
wrapper.__name__ = func.__name__
return wrapper
Also I put it all in a in a decorator so I can hide all my apis behind it.

Related

How to validate Keycloak webhook request?

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

How to add custom Header in Authlib on Django

I would need some of your help adapting Authlib with Django.
I'm trying to develop a Django app using OpenId and Authlib to connect my users and facing an issue with the access token, the issue invalid_client occurs. Using Postman I found out that the OpenId provider needs some parameters in the Header like 'Content-Length' or 'Host'.
When the Header param is defined in client.py, it works like a charm. However, I'd like to pass the custom header from views.py (mostly to avoid defining the Host directly in the package), but authorize_access_token doesn't allow multiple arguments,
def auth(request):
token = oauth.customprovider.authorize_access_token(request)
Maybe the "Compliance Fix for non Standard" feature might help, but I wasn't able to adapt it for Django and the Header parameter
https://docs.authlib.org/en/stable/client/oauth2.html#compliance-fix-oauth2
from authlib.common.urls import add_params_to_uri, url_decode
def _non_compliant_param_name(url, headers, data):
params = {'site': 'stackoverflow'}
url = add_params_to_uri(url, params)
return url, headers, body
def _fix_token_response(resp):
data = dict(url_decode(resp.text))
data['token_type'] = 'Bearer'
data['expires_in'] = int(data['expires'])
resp.json = lambda: data
return resp
session.register_compliance_hook(
'protected_request', _non_compliant_param_name)
session.register_compliance_hook(
'access_token_response', _fix_token_response)
Does anyone know a way to pass a custom Header to Authlib or defining it using the Compliance Fix and Django?
I had to do this recently for a provider that required an Authorization header added to the the refresh token. Here is the code I used.
Add the register_compliance_hook inside the function that is called using the compliance_fix argument when initializing the service.
def _compliance_fixes(session):
def _add_header_refresh(url, headers, body):
headers.update({'Authorization': "Basic " + self.secret_client_key})
return url, headers, body
session.register_compliance_hook('refresh_token_request', _add_header_refresh)
oauth = OAuth()
oauth.register("oauth-service", compliance_fix=_compliance_fixes)

Flask JWT : get_jwt_identity() without #jwt_required always return Non

I am building a Flask JWT extended application, and here is what I want to do :
I have and endpoint open to everyone (i.e. no decorator #jwt_required, everyone can access the link)
but if someone access this link with an authentication, I want to be able to get back his identity, something like that :
#bp.route('/test', methods=('GET', 'POST'))
def test() -> str:
identity = get_jwt_identity()
print(a)
if identity is not None:
# do something
else:
# do other stuff
the issue is that without the decorator, get_jwt_identity always return None
Is there a way to accomplish that?
Thanks
If you are using the 4.x.x version, use #jwt_required(optional=True). If you are still on the 3.x.x version use #jwt_optional
If you want to decode custom access token on unprotected endpoint. you need to use the decode_token from flask-jwt-extended
from flask_jwt_extended import decode_token
def post(self):
data = ResetPassword.parser.parse_args()
reset_token = data['reset_token']
decoded_token = decode_token(reset_token)['sub']
the sub refers to `identity when you're creating a token from
access_token = create_access_token(identity=user.get_id())

Adding custom response Headers to APIException

I have created a custom exception referring to http://django-rest-framework.org/api-guide/exceptions.html.
Please know that I have my own authentication backend. Hence I am not using rest_framework's authentication module.
For authentication errors, I want to add 'WWW-Authenticate: Token' header to the response that is sent from the exception.
Any ideas will be very helpful.
Update:
Thanks #Pathétique,
This is what I ended up doing.
-Have a base view class named BaseView.
-override the handle_exception method to set appropriate headers, in my case 'WWW-Authenticate'.
Here is the code:
class BaseView(APIView):
def handle_exception(self, exc):
if isinstance(exc, MYEXCEPTION):
self.headers['WWW-Authenticate'] = "Token"
return Response({'detail': exc.detail,
status=exc.status_code, exception=True)
Your thoughts?
Try overriding finalize_response in your rest framework view:
def finalize_response(self, request, *args, **kwargs):
response = super(SomeAPIView, self).finalize_response(request, *args, **kwargs)
response['WWW-Authenticate'] = 'Token'
return response
Edit:
After seeing your update, I think your override of handle_exception should work, I would only add an else statement to call the parent method to cover other exceptions. One thing I noticed in overriding dispatch, which may not be an issue here, is that setting a new key/value for self.headers resulted in a server error that I didn't take the time to track down. Anyways, it seems you are on the right track.
Use the authenticate_header method on your authentication class.
Additionally that'll ensure your responses also have the right 401 Unauthorized status code set, instead of 403 Forbidden.
See here: http://django-rest-framework.org/api-guide/authentication.html#custom-authentication
Your solution is quite correct, in my case I found it more appropiate to add the header and then call the method on the super instance, to maintain default behaviour:
class BaseView(APIView):
def handle_exception(self, exc):
if isinstance(exc, MYEXCEPTION):
self.headers['WWW-Authenticate'] = "Token"
return super().handle_exception(excepto)

django/python the works of views and bringing in an API

I'm just beginning to learn about python/django. I know PHP, but I wanted to get to know about this framework. I'm trying to work with yelp's API. I'm trying to figure out what to do when someone brings in a new file into the project.
In their business.py they have this:
import json
import oauth2
import optparse
import urllib
import urllib2
parser = optparse.OptionParser()
parser.add_option('-c', '--consumer_key', dest='consumer_key', help='OAuth consumer key (REQUIRED)')
parser.add_option('-s', '--consumer_secret', dest='consumer_secret', help='OAuth consumer secret (REQUIRED)')
parser.add_option('-t', '--token', dest='token', help='OAuth token (REQUIRED)')
parser.add_option('-e', '--token_secret', dest='token_secret', help='OAuth token secret (REQUIRED)')
parser.add_option('-a', '--host', dest='host', help='Host', default='api.yelp.com')
parser.add_option('-i', '--id', dest='id', help='Business')
parser.add_option('-u', '--cc', dest='cc', help='Country code')
parser.add_option('-n', '--lang', dest='lang', help='Language code')
options, args = parser.parse_args()
# Required options
if not options.consumer_key:
parser.error('--consumer_key required')
if not options.consumer_secret:
parser.error('--consumer_secret required')
if not options.token:
parser.error('--token required')
if not options.token_secret:
parser.error('--token_secret required')
if not options.id:
parser.error('--id required')
url_params = {}
if options.cc:
url_params['cc'] = options.cc
if options.lang:
url_params['lang'] = options.lang
path = '/v2/business/%s' % (options.id,)
def request(host, path, url_params, consumer_key, consumer_secret, token, token_secret):
"""Returns response for API request."""
# Unsigned URL
encoded_params = ''
if url_params:
encoded_params = urllib.urlencode(url_params)
url = 'http://%s%s?%s' % (host, path, encoded_params)
print 'URL: %s' % (url,)
# Sign the URL
consumer = oauth2.Consumer(consumer_key, consumer_secret)
oauth_request = oauth2.Request('GET', url, {})
oauth_request.update({'oauth_nonce': oauth2.generate_nonce(),
'oauth_timestamp': oauth2.generate_timestamp(),
'oauth_token': token,
'oauth_consumer_key': consumer_key})
token = oauth2.Token(token, token_secret)
oauth_request.sign_request(oauth2.SignatureMethod_HMAC_SHA1(), consumer, token)
signed_url = oauth_request.to_url()
print 'Signed URL: %s\n' % (signed_url,)
# Connect
try:
conn = urllib2.urlopen(signed_url, None)
try:
response = json.loads(conn.read())
finally:
conn.close()
except urllib2.HTTPError, error:
response = json.loads(error.read())
return response
response = request(options.host, path, url_params, options.consumer_key, options.consumer_secret, options.token, options.token_secret)
print json.dumps(response, sort_keys=True, indent=2)
Its very lengthy, I appologize for that. But my concern is what do I do with this? They've set up a def request() in here, and I'm assuming that I have to import this into my views?
I've been following the django documentation of creating a new app. In the documentation they've set up a bunch of def inside the views.py file. I'm just confused as to how am I supposed to make this work with my project? If I wanted to search for a business in the URL, how would it send the data out?
Thanks for your help.
This is a command line script that makes http requests to the yelp api. You probably don't want to make such an external request within the context of a main request handler. Well, you could call a request handler that makes this call to yelp. Let's see ...
You could import the request function and instead of invoking it with command line options, call it yourself.
from yelp.business import request as yelp_req
def my_request_handler(request):
json_from_yelp = yelp_req(...
# do stuff and return a response
Making this kind of external call inside a request handler is pretty meh though (that is, making an http request to an external service within a request handler). If the call is in ajax, it may be ok for the ux.
This business.py is just an example showing you how to create a signed request with oauth2. You may be able to just import the request function and use it. OTOH, you may prefer to write your own (perhaps using the requests library). You probably want to use celery or some other async means to make the calls outside of your request handlers and/or cache the responses to avoid costly external http io with every request.

Categories