Skip to content

Commit 72b2404

Browse files
committed
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.
1 parent c5eb9f1 commit 72b2404

File tree

2 files changed

+37
-3
lines changed

2 files changed

+37
-3
lines changed

oauth2client/client.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1784,14 +1784,17 @@ class OAuth2WebServerFlow(Flow):
17841784
"""
17851785

17861786
@util.positional(4)
1787-
def __init__(self, client_id, client_secret, scope,
1787+
def __init__(self, client_id,
1788+
client_secret=None,
1789+
scope=None,
17881790
redirect_uri=None,
17891791
user_agent=None,
17901792
auth_uri=GOOGLE_AUTH_URI,
17911793
token_uri=GOOGLE_TOKEN_URI,
17921794
revoke_uri=GOOGLE_REVOKE_URI,
17931795
login_hint=None,
17941796
device_uri=GOOGLE_DEVICE_URI,
1797+
authorization_header=None,
17951798
**kwargs):
17961799
"""Constructor for OAuth2WebServerFlow.
17971800
@@ -1819,9 +1822,14 @@ def __init__(self, client_id, client_secret, scope,
18191822
proper multi-login session, thereby simplifying the login flow.
18201823
device_uri: string, URI for device authorization endpoint. For convenience
18211824
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
1825+
authorization_header: string, For use with OAuth 2.0 providers that
1826+
require a client to authenticate using a header value instead of passing
1827+
client_secret in the POST body.
18221828
**kwargs: dict, The keyword arguments are all optional and required
18231829
parameters for the OAuth calls.
18241830
"""
1831+
if scope is None:
1832+
raise TypeError("The value of scope must not be None")
18251833
self.client_id = client_id
18261834
self.client_secret = client_secret
18271835
self.scope = util.scopes_to_string(scope)
@@ -1832,6 +1840,7 @@ def __init__(self, client_id, client_secret, scope,
18321840
self.token_uri = token_uri
18331841
self.revoke_uri = revoke_uri
18341842
self.device_uri = device_uri
1843+
self.authorization_header = authorization_header
18351844
self.params = {
18361845
'access_type': 'offline',
18371846
'response_type': 'code',
@@ -1957,10 +1966,11 @@ def step2_exchange(self, code=None, http=None, device_flow_info=None):
19571966

19581967
post_data = {
19591968
'client_id': self.client_id,
1960-
'client_secret': self.client_secret,
19611969
'code': code,
19621970
'scope': self.scope,
19631971
}
1972+
if self.client_secret is not None:
1973+
post_data['client_secret'] = self.client_secret
19641974
if device_flow_info is not None:
19651975
post_data['grant_type'] = 'http://oauth.net/grant_type/device/1.0'
19661976
else:
@@ -1970,7 +1980,8 @@ def step2_exchange(self, code=None, http=None, device_flow_info=None):
19701980
headers = {
19711981
'content-type': 'application/x-www-form-urlencoded',
19721982
}
1973-
1983+
if self.authorization_header is not None:
1984+
headers['Authorization'] = self.authorization_header
19741985
if self.user_agent is not None:
19751986
headers['user-agent'] = self.user_agent
19761987

tests/test_oauth2client.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,6 +1027,29 @@ def __contains__(self, name):
10271027
request_code = urllib.parse.parse_qs(http.requests[0]['body'])['code'][0]
10281028
self.assertEqual(code, request_code)
10291029

1030+
def test_exchange_using_authorization_header(self):
1031+
auth_header = 'Basic Y2xpZW50X2lkKzE6c2VjcmV0KzE=',
1032+
flow = OAuth2WebServerFlow(
1033+
client_id='client_id+1',
1034+
authorization_header=auth_header,
1035+
scope='foo',
1036+
redirect_uri=OOB_CALLBACK_URN,
1037+
user_agent='unittest-sample/1.0',
1038+
revoke_uri='dummy_revoke_uri',
1039+
)
1040+
http = HttpMockSequence([
1041+
({'status': '200'}, b'access_token=SlAV32hkKG'),
1042+
])
1043+
1044+
credentials = flow.step2_exchange('some random code', http=http)
1045+
self.assertEqual('SlAV32hkKG', credentials.access_token)
1046+
1047+
test_request = http.requests[0]
1048+
# Did we pass the Authorization header?
1049+
self.assertEqual(test_request['headers']['Authorization'], auth_header)
1050+
# Did we omit client_secret from POST body?
1051+
self.assertNotIn('client_secret', test_request['body'])
1052+
10301053
def test_urlencoded_exchange_success(self):
10311054
http = HttpMockSequence([
10321055
({'status': '200'}, b'access_token=SlAV32hkKG&expires_in=3600'),

0 commit comments

Comments
 (0)