From 72b240453a9932cb09700871f22e499c1368085a Mon Sep 17 00:00:00 2001 From: Brendan McCollam Date: Tue, 23 Jun 2015 16:29:49 -0500 Subject: [PATCH] Support authentication using Authorization header According to RFC 6749, section 2.3.1: > Including the client credentials in the request-body using the two > parameters is NOT RECOMMENDED and SHOULD be limited to clients unable > to directly utilize the HTTP Basic authentication scheme (or other > password-based HTTP authentication schemes). This changeset adds an optional parameter, `authorization_header` that makes it easier to use this client library with OAuth2 providers that support client authentication via the HTTP Authorization header instead of in the request body. --- oauth2client/client.py | 17 ++++++++++++++--- tests/test_oauth2client.py | 23 +++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/oauth2client/client.py b/oauth2client/client.py index 2613eb1..37b8483 100644 --- a/oauth2client/client.py +++ b/oauth2client/client.py @@ -1784,7 +1784,9 @@ class OAuth2WebServerFlow(Flow): """ @util.positional(4) - def __init__(self, client_id, client_secret, scope, + def __init__(self, client_id, + client_secret=None, + scope=None, redirect_uri=None, user_agent=None, auth_uri=GOOGLE_AUTH_URI, @@ -1792,6 +1794,7 @@ class OAuth2WebServerFlow(Flow): revoke_uri=GOOGLE_REVOKE_URI, login_hint=None, device_uri=GOOGLE_DEVICE_URI, + authorization_header=None, **kwargs): """Constructor for OAuth2WebServerFlow. @@ -1819,9 +1822,14 @@ class OAuth2WebServerFlow(Flow): proper multi-login session, thereby simplifying the login flow. device_uri: string, URI for device authorization endpoint. For convenience defaults to Google's endpoints but any OAuth 2.0 provider can be used. + authorization_header: string, For use with OAuth 2.0 providers that + require a client to authenticate using a header value instead of passing + client_secret in the POST body. **kwargs: dict, The keyword arguments are all optional and required parameters for the OAuth calls. """ + if scope is None: + raise TypeError("The value of scope must not be None") self.client_id = client_id self.client_secret = client_secret self.scope = util.scopes_to_string(scope) @@ -1832,6 +1840,7 @@ class OAuth2WebServerFlow(Flow): self.token_uri = token_uri self.revoke_uri = revoke_uri self.device_uri = device_uri + self.authorization_header = authorization_header self.params = { 'access_type': 'offline', 'response_type': 'code', @@ -1957,10 +1966,11 @@ class OAuth2WebServerFlow(Flow): post_data = { 'client_id': self.client_id, - 'client_secret': self.client_secret, 'code': code, 'scope': self.scope, } + if self.client_secret is not None: + post_data['client_secret'] = self.client_secret if device_flow_info is not None: post_data['grant_type'] = 'http://oauth.net/grant_type/device/1.0' else: @@ -1970,7 +1980,8 @@ class OAuth2WebServerFlow(Flow): headers = { 'content-type': 'application/x-www-form-urlencoded', } - + if self.authorization_header is not None: + headers['Authorization'] = self.authorization_header if self.user_agent is not None: headers['user-agent'] = self.user_agent diff --git a/tests/test_oauth2client.py b/tests/test_oauth2client.py index e0819fd..311fcc0 100755 --- a/tests/test_oauth2client.py +++ b/tests/test_oauth2client.py @@ -1027,6 +1027,29 @@ class OAuth2WebServerFlowTest(unittest.TestCase): request_code = urllib.parse.parse_qs(http.requests[0]['body'])['code'][0] self.assertEqual(code, request_code) + def test_exchange_using_authorization_header(self): + auth_header = 'Basic Y2xpZW50X2lkKzE6c2VjcmV0KzE=', + flow = OAuth2WebServerFlow( + client_id='client_id+1', + authorization_header=auth_header, + scope='foo', + redirect_uri=OOB_CALLBACK_URN, + user_agent='unittest-sample/1.0', + revoke_uri='dummy_revoke_uri', + ) + http = HttpMockSequence([ + ({'status': '200'}, b'access_token=SlAV32hkKG'), + ]) + + credentials = flow.step2_exchange('some random code', http=http) + self.assertEqual('SlAV32hkKG', credentials.access_token) + + test_request = http.requests[0] + # Did we pass the Authorization header? + self.assertEqual(test_request['headers']['Authorization'], auth_header) + # Did we omit client_secret from POST body? + self.assertNotIn('client_secret', test_request['body']) + def test_urlencoded_exchange_success(self): http = HttpMockSequence([ ({'status': '200'}, b'access_token=SlAV32hkKG&expires_in=3600'),