Skip to content

Commit b3c227a

Browse files
Merge pull request #357 from singingwolfboy/oauthlib3
Switch to OAuthlib 3.0.0
2 parents e859dbd + 53481e3 commit b3c227a

File tree

6 files changed

+272
-80
lines changed

6 files changed

+272
-80
lines changed

HISTORY.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ History
44
UNRELEASED
55
++++++++++
66

7+
- This project now depends on OAuthlib 3.0.0 and above. It does **not** support
8+
versions of OAuthlib before 3.0.0.
9+
- Updated oauth2 tests to use 'sess' for an OAuth2Session instance instead of `auth`
10+
because OAuth2Session objects and methods acceept an `auth` paramether which is
11+
typically an instance of `requests.auth.HTTPBasicAuth`
12+
- `OAuth2Session.fetch_token` previously tried to guess how and where to provide
13+
"client" and "user" credentials incorrectly. This was incompatible with some
14+
OAuth servers and incompatible with breaking changes in oauthlib that seek to
15+
correctly provide the `client_id`. The older implementation also did not raise
16+
the correct exceptions when username and password are not present on Legacy
17+
clients.
718
- Avoid automatic netrc authentication for OAuth2Session.
819

920
v1.1.0 (9 January 2019)

requests_oauthlib/oauth2_session.py

Lines changed: 83 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from oauthlib.common import generate_token, urldecode
66
from oauthlib.oauth2 import WebApplicationClient, InsecureTransportError
7+
from oauthlib.oauth2 import LegacyApplicationClient
78
from oauthlib.oauth2 import TokenExpiredError, is_secure_transport
89
import requests
910

@@ -158,11 +159,14 @@ def authorization_url(self, url, state=None, **kwargs):
158159

159160
def fetch_token(self, token_url, code=None, authorization_response=None,
160161
body='', auth=None, username=None, password=None, method='POST',
161-
timeout=None, headers=None, verify=True, proxies=None, **kwargs):
162+
timeout=None, headers=None, verify=True, proxies=None,
163+
include_client_id=None, client_secret=None, **kwargs):
162164
"""Generic method for fetching an access token from the token endpoint.
163165
164166
If you are using the MobileApplicationClient you will want to use
165-
token_from_fragment instead of fetch_token.
167+
`token_from_fragment` instead of `fetch_token`.
168+
169+
The current implementation enforces the RFC guidelines.
166170
167171
:param token_url: Token endpoint URL, must use HTTPS.
168172
:param code: Authorization code (used by WebApplicationClients).
@@ -171,15 +175,28 @@ def fetch_token(self, token_url, code=None, authorization_response=None,
171175
WebApplicationClients instead of code.
172176
:param body: Optional application/x-www-form-urlencoded body to add the
173177
include in the token request. Prefer kwargs over body.
174-
:param auth: An auth tuple or method as accepted by requests.
175-
:param username: Username used by LegacyApplicationClients.
176-
:param password: Password used by LegacyApplicationClients.
178+
:param auth: An auth tuple or method as accepted by `requests`.
179+
:param username: Username required by LegacyApplicationClients to appear
180+
in the request body.
181+
:param password: Password required by LegacyApplicationClients to appear
182+
in the request body.
177183
:param method: The HTTP method used to make the request. Defaults
178184
to POST, but may also be GET. Other methods should
179185
be added as needed.
180-
:param headers: Dict to default request headers with.
181186
:param timeout: Timeout of the request in seconds.
187+
:param headers: Dict to default request headers with.
182188
:param verify: Verify SSL certificate.
189+
:param proxies: The `proxies` argument is passed onto `requests`.
190+
:param include_client_id: Should the request body include the
191+
`client_id` parameter. Default is `None`,
192+
which will attempt to autodetect. This can be
193+
forced to always include (True) or never
194+
include (False).
195+
:param client_secret: The `client_secret` paired to the `client_id`.
196+
This is generally required unless provided in the
197+
`auth` tuple. If the value is `None`, it will be
198+
omitted from the request, however if the value is
199+
an empty string, an empty string will be sent.
183200
:param kwargs: Extra parameters to include in the token request.
184201
:return: A token dict
185202
"""
@@ -196,23 +213,65 @@ def fetch_token(self, token_url, code=None, authorization_response=None,
196213
raise ValueError('Please supply either code or '
197214
'authorization_response parameters.')
198215

216+
# Earlier versions of this library build an HTTPBasicAuth header out of
217+
# `username` and `password`. The RFC states, however these attributes
218+
# must be in the request body and not the header.
219+
# If an upstream server is not spec compliant and requires them to
220+
# appear as an Authorization header, supply an explicit `auth` header
221+
# to this function.
222+
# This check will allow for empty strings, but not `None`.
223+
#
224+
# Refernences
225+
# 4.3.2 - Resource Owner Password Credentials Grant
226+
# https://tools.ietf.org/html/rfc6749#section-4.3.2
227+
228+
if isinstance(self._client, LegacyApplicationClient):
229+
if username is None:
230+
raise ValueError('`LegacyApplicationClient` requires both the '
231+
'`username` and `password` parameters.')
232+
if password is None:
233+
raise ValueError('The required paramter `username` was supplied, '
234+
'but `password` was not.')
235+
236+
# merge username and password into kwargs for `prepare_request_body`
237+
if username is not None:
238+
kwargs['username'] = username
239+
if password is not None:
240+
kwargs['password'] = password
241+
242+
# is an auth explicitly supplied?
243+
if auth is not None:
244+
# if we're dealing with the default of `include_client_id` (None):
245+
# we will assume the `auth` argument is for an RFC compliant server
246+
# and we should not send the `client_id` in the body.
247+
# This approach allows us to still force the client_id by submitting
248+
# `include_client_id=True` along with an `auth` object.
249+
if include_client_id is None:
250+
include_client_id = False
251+
252+
# otherwise we may need to create an auth header
253+
else:
254+
# since we don't have an auth header, we MAY need to create one
255+
# it is possible that we want to send the `client_id` in the body
256+
# if so, `include_client_id` should be set to True
257+
# otherwise, we will generate an auth header
258+
if include_client_id is not True:
259+
client_id = self.client_id
260+
if client_id:
261+
log.debug('Encoding `client_id` "%s" with `client_secret` '
262+
'as Basic auth credentials.', client_id)
263+
client_secret = client_secret if client_secret is not None else ''
264+
auth = requests.auth.HTTPBasicAuth(client_id, client_secret)
265+
266+
if include_client_id:
267+
# this was pulled out of the params
268+
# it needs to be passed into prepare_request_body
269+
if client_secret is not None:
270+
kwargs['client_secret'] = client_secret
199271

200272
body = self._client.prepare_request_body(code=code, body=body,
201-
redirect_uri=self.redirect_uri, username=username,
202-
password=password, **kwargs)
203-
204-
client_id = kwargs.get('client_id', '')
205-
if auth is None:
206-
if client_id:
207-
log.debug('Encoding client_id "%s" with client_secret as Basic auth credentials.', client_id)
208-
client_secret = kwargs.get('client_secret', '')
209-
client_secret = client_secret if client_secret is not None else ''
210-
auth = requests.auth.HTTPBasicAuth(client_id, client_secret)
211-
elif username:
212-
if password is None:
213-
raise ValueError('Username was supplied, but not password.')
214-
log.debug('Encoding username, password as Basic auth credentials.')
215-
auth = requests.auth.HTTPBasicAuth(username, password)
273+
redirect_uri=self.redirect_uri,
274+
include_client_id=include_client_id, **kwargs)
216275

217276
headers = headers or {
218277
'Accept': 'application/json',
@@ -269,9 +328,11 @@ def refresh_token(self, token_url, refresh_token=None, body='', auth=None,
269328
:param refresh_token: The refresh_token to use.
270329
:param body: Optional application/x-www-form-urlencoded body to add the
271330
include in the token request. Prefer kwargs over body.
272-
:param auth: An auth tuple or method as accepted by requests.
331+
:param auth: An auth tuple or method as accepted by `requests`.
273332
:param timeout: Timeout of the request in seconds.
333+
:param headers: A dict of headers to be used by `requests`.
274334
:param verify: Verify SSL certificate.
335+
:param proxies: The `proxies` argument will be passed to `requests`.
275336
:param kwargs: Extra parameters to include in the token request.
276337
:return: A token dict
277338
"""

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
requests>=2.0.0
2-
oauthlib[signedtoken]>=2.1.0,<3.0.0
2+
oauthlib[signedtoken]>=3.0.0

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ def readall(path):
4444
url='https://github.com/requests/requests-oauthlib',
4545
packages=['requests_oauthlib', 'requests_oauthlib.compliance_fixes'],
4646
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
47-
install_requires=['oauthlib>=2.1.0,<3.0.0', 'requests>=2.0.0'],
48-
extras_require={'rsa': ['oauthlib[signedtoken]>=2.1.0,<3.0.0']},
47+
install_requires=['oauthlib>=3.0.0', 'requests>=2.0.0'],
48+
extras_require={'rsa': ['oauthlib[signedtoken]>=3.0.0']},
4949
license='ISC',
5050
classifiers=[
5151
'Development Status :: 5 - Production/Stable',

tests/test_compliance_fixes.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ def setUp(self):
3232
mocker.start()
3333
self.addCleanup(mocker.stop)
3434

35-
facebook = OAuth2Session('foo', redirect_uri='https://i.b')
35+
facebook = OAuth2Session('someclientid', redirect_uri='https://i.b')
3636
self.session = facebook_compliance_fix(facebook)
3737

3838
def test_fetch_access_token(self):
3939
token = self.session.fetch_token(
4040
'https://graph.facebook.com/oauth/access_token',
41-
client_secret='bar',
41+
client_secret='someclientsecret',
4242
authorization_response='https://i.b/?code=hello',
4343
)
4444
self.assertEqual(token, {'access_token': 'urlencoded', 'token_type': 'Bearer'})
@@ -55,15 +55,15 @@ def setUp(self):
5555
self.mocker.start()
5656
self.addCleanup(self.mocker.stop)
5757

58-
fitbit = OAuth2Session('foo', redirect_uri='https://i.b')
58+
fitbit = OAuth2Session('someclientid', redirect_uri='https://i.b')
5959
self.session = fitbit_compliance_fix(fitbit)
6060

6161
def test_fetch_access_token(self):
6262
self.assertRaises(
6363
InvalidGrantError,
6464
self.session.fetch_token,
6565
'https://api.fitbit.com/oauth2/token',
66-
client_secret='bar',
66+
client_secret='someclientsecret',
6767
authorization_response='https://i.b/?code=hello',
6868
)
6969

@@ -84,7 +84,7 @@ def test_refresh_token(self):
8484
InvalidGrantError,
8585
self.session.refresh_token,
8686
'https://api.fitbit.com/oauth2/token',
87-
auth=requests.auth.HTTPBasicAuth('foo', 'bar')
87+
auth=requests.auth.HTTPBasicAuth('someclientid', 'someclientsecret')
8888
)
8989

9090
self.mocker.post(
@@ -94,7 +94,7 @@ def test_refresh_token(self):
9494

9595
token = self.session.refresh_token(
9696
'https://api.fitbit.com/oauth2/token',
97-
auth=requests.auth.HTTPBasicAuth('foo', 'bar')
97+
auth=requests.auth.HTTPBasicAuth('someclientid', 'someclientsecret')
9898
)
9999

100100
self.assertEqual(token['access_token'], 'access')
@@ -120,13 +120,13 @@ def setUp(self):
120120
mocker.start()
121121
self.addCleanup(mocker.stop)
122122

123-
linkedin = OAuth2Session('foo', redirect_uri='https://i.b')
123+
linkedin = OAuth2Session('someclientid', redirect_uri='https://i.b')
124124
self.session = linkedin_compliance_fix(linkedin)
125125

126126
def test_fetch_access_token(self):
127127
token = self.session.fetch_token(
128128
'https://www.linkedin.com/uas/oauth2/accessToken',
129-
client_secret='bar',
129+
client_secret='someclientsecret',
130130
authorization_response='https://i.b/?code=hello',
131131
)
132132
self.assertEqual(token, {'access_token': 'linkedin', 'token_type': 'Bearer'})
@@ -152,13 +152,13 @@ def setUp(self):
152152
mocker.start()
153153
self.addCleanup(mocker.stop)
154154

155-
mailchimp = OAuth2Session('foo', redirect_uri='https://i.b')
155+
mailchimp = OAuth2Session('someclientid', redirect_uri='https://i.b')
156156
self.session = mailchimp_compliance_fix(mailchimp)
157157

158158
def test_fetch_access_token(self):
159159
token = self.session.fetch_token(
160160
"https://login.mailchimp.com/oauth2/token",
161-
client_secret='bar',
161+
client_secret='someclientsecret',
162162
authorization_response='https://i.b/?code=hello',
163163
)
164164
# Times should be close
@@ -184,13 +184,13 @@ def setUp(self):
184184
mocker.start()
185185
self.addCleanup(mocker.stop)
186186

187-
weibo = OAuth2Session('foo', redirect_uri='https://i.b')
187+
weibo = OAuth2Session('someclientid', redirect_uri='https://i.b')
188188
self.session = weibo_compliance_fix(weibo)
189189

190190
def test_fetch_access_token(self):
191191
token = self.session.fetch_token(
192192
'https://api.weibo.com/oauth2/access_token',
193-
client_secret='bar',
193+
client_secret='someclientsecret',
194194
authorization_response='https://i.b/?code=hello',
195195
)
196196
self.assertEqual(token, {'access_token': 'weibo', 'token_type': 'Bearer'})
@@ -223,7 +223,7 @@ def setUp(self):
223223
mocker.start()
224224
self.addCleanup(mocker.stop)
225225

226-
slack = OAuth2Session('foo', redirect_uri='https://i.b')
226+
slack = OAuth2Session('someclientid', redirect_uri='https://i.b')
227227
self.session = slack_compliance_fix(slack)
228228

229229
def test_protected_request(self):
@@ -293,7 +293,7 @@ def setUp(self):
293293
mocker.start()
294294
self.addCleanup(mocker.stop)
295295

296-
plentymarkets = OAuth2Session('foo', redirect_uri='https://i.b')
296+
plentymarkets = OAuth2Session('someclientid', redirect_uri='https://i.b')
297297
self.session = plentymarkets_compliance_fix(plentymarkets)
298298

299299
def test_fetch_access_token(self):

0 commit comments

Comments
 (0)