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