From fb8e718c446cc1aae943a41226a01361e578b21d Mon Sep 17 00:00:00 2001 From: Joseph Schorr <joseph.schorr@coreos.com> Date: Mon, 18 May 2015 12:38:39 -0400 Subject: [PATCH] Fix OAuth 2 handler to support retrieving parameters from other places; various OAuth client (such as the Go library) send the values in the request body or even the Auth header --- endpoints/common.py | 5 +++-- endpoints/web.py | 28 ++++++++++++++++------------ util/headers.py | 16 ++++++++++++++++ 3 files changed, 35 insertions(+), 14 deletions(-) create mode 100644 util/headers.py diff --git a/endpoints/common.py b/endpoints/common.py index ec61b5adf..131c9f080 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -89,12 +89,13 @@ def truthy_param(param): return param not in {False, 'false', 'False', '0', 'FALSE', '', 'null'} -def param_required(param_name): +def param_required(param_name, allow_body=False): def wrapper(wrapped): @wraps(wrapped) def decorated(*args, **kwargs): if param_name not in request.args: - abort(make_response('Required param: %s' % param_name, 400)) + if not allow_body and param_name not in request.values: + abort(make_response('Required param: %s' % param_name, 400)) return wrapped(*args, **kwargs) return decorated return wrapper diff --git a/endpoints/web.py b/endpoints/web.py index 9d552fe1c..7a460568d 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -25,6 +25,7 @@ from endpoints.trigger import (CustomBuildTrigger, BitbucketBuildTrigger, Trigge from util.names import parse_repository_name, parse_repository_name_and_tag from util.useremails import send_email_changed from util.systemlogs import build_logs_archive +from util.headers import parse_basic_auth from auth import scopes import features @@ -462,19 +463,22 @@ def request_authorization_code(): @web.route('/oauth/access_token', methods=['POST']) @no_cache -@param_required('grant_type') -@param_required('client_id') -@param_required('client_secret') -@param_required('redirect_uri') -@param_required('code') -@param_required('scope') +@param_required('grant_type', allow_body=True) +@param_required('client_id', allow_body=True) +@param_required('redirect_uri', allow_body=True) +@param_required('code', allow_body=True) +@param_required('scope', allow_body=True) def exchange_code_for_token(): - grant_type = request.form.get('grant_type', None) - client_id = request.form.get('client_id', None) - client_secret = request.form.get('client_secret', None) - redirect_uri = request.form.get('redirect_uri', None) - code = request.form.get('code', None) - scope = request.form.get('scope', None) + grant_type = request.values.get('grant_type', None) + client_id = request.values.get('client_id', None) + redirect_uri = request.values.get('redirect_uri', None) + code = request.values.get('code', None) + scope = request.values.get('scope', None) + + client_secret = request.values.get('client_secret', None) + if client_secret is None: + # Sometimes OAuth2 clients place the client secret in the Auth header. + client_secret = parse_basic_auth(request.headers.get('Authorization')) provider = FlaskAuthorizationProvider() return provider.get_token(grant_type, client_id, client_secret, redirect_uri, code, scope=scope) diff --git a/util/headers.py b/util/headers.py new file mode 100644 index 000000000..ae53c003a --- /dev/null +++ b/util/headers.py @@ -0,0 +1,16 @@ +import base64 + +def parse_basic_auth(header_value): + """ Attempts to parse the given header value as a Base64-encoded Basic auth header. """ + + if not header_value: + return None + + parts = header_value.split(' ') + if len(parts) != 2 or parts[0].lower() != 'basic': + return None + + try: + return base64.b64decode(parts[1]) + except ValueError: + return None \ No newline at end of file