Skip to content

Commit b7f0bfd

Browse files
anthonyweejmoldow
authored andcommitted
Change downscope_token() to return a TokenResponse object (#237)
Add a `TokenResponse` object, which is a subclass of `BaseAPIJSONObject`. `OAuth2.downscope_token()` now returns a `TokenResponse` object, which allows users to get other fields of the response, such as 'expires_in'. Bump version to 2.0.0a9.
1 parent e65d34c commit b7f0bfd

File tree

3 files changed

+75
-68
lines changed

3 files changed

+75
-68
lines changed

boxsdk/auth/oauth2.py

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
import six
1212
from six.moves.urllib.parse import urlencode, urlunsplit # pylint:disable=import-error,no-name-in-module
1313

14-
from boxsdk.network.default_network import DefaultNetwork
1514
from boxsdk.config import API
1615
from boxsdk.exception import BoxOAuthException
16+
from boxsdk.network.default_network import DefaultNetwork
17+
from boxsdk.object.base_api_json_object import BaseAPIJSONObject
1718
from boxsdk.util.text_enum import TextEnum
1819

1920

@@ -31,6 +32,11 @@ class TokenScope(TextEnum):
3132
ITEM_DOWNLOAD = 'item_download'
3233

3334

35+
class TokenResponse(BaseAPIJSONObject):
36+
""" Represents the response for a token request. """
37+
pass
38+
39+
3440
class OAuth2(object):
3541
"""
3642
Responsible for handling OAuth2 for the Box API. Can authenticate and refresh tokens.
@@ -294,7 +300,7 @@ def _update_current_tokens(self, access_token, refresh_token):
294300
"""
295301
self._access_token, self._refresh_token = access_token, refresh_token
296302

297-
def _send_token_request_without_storing_tokens(self, data, access_token, expect_refresh_token=True):
303+
def _execute_token_request(self, data, access_token, expect_refresh_token=True):
298304
"""
299305
Send the request to acquire or refresh an access token.
300306
@@ -307,9 +313,9 @@ def _send_token_request_without_storing_tokens(self, data, access_token, expect_
307313
:type access_token:
308314
`unicode` or None
309315
:return:
310-
The access token and refresh token.
316+
The response for the token request.
311317
:rtype:
312-
(`unicode`, `unicode`)
318+
:class:`TokenResponse`
313319
"""
314320
self._check_closed()
315321
url = '{base_auth_url}/token'.format(base_auth_url=API.OAUTH2_API_URL)
@@ -324,15 +330,14 @@ def _send_token_request_without_storing_tokens(self, data, access_token, expect_
324330
if not network_response.ok:
325331
raise BoxOAuthException(network_response.status_code, network_response.content, url, 'POST')
326332
try:
327-
response = network_response.json()
328-
access_token = response['access_token']
329-
refresh_token = response.get('refresh_token', None)
330-
if refresh_token is None and expect_refresh_token:
331-
raise BoxOAuthException(network_response.status_code, network_response.content, url, 'POST')
332-
except (ValueError, KeyError):
333+
token_response = TokenResponse(network_response.json())
334+
except ValueError:
333335
raise BoxOAuthException(network_response.status_code, network_response.content, url, 'POST')
334336

335-
return access_token, refresh_token
337+
if ('access_token' not in token_response) or (expect_refresh_token and 'refresh_token' not in token_response):
338+
raise BoxOAuthException(network_response.status_code, network_response.content, url, 'POST')
339+
340+
return token_response
336341

337342
def send_token_request(self, data, access_token, expect_refresh_token=True):
338343
"""
@@ -351,8 +356,10 @@ def send_token_request(self, data, access_token, expect_refresh_token=True):
351356
:rtype:
352357
(`unicode`, `unicode`)
353358
"""
354-
access_token, refresh_token = self._send_token_request_without_storing_tokens(data, access_token, expect_refresh_token)
355-
self._store_tokens(access_token, refresh_token)
359+
token_response = self._execute_token_request(data, access_token, expect_refresh_token)
360+
# pylint:disable=no-member
361+
refresh_token = token_response.refresh_token if 'refresh_token' in token_response else None
362+
self._store_tokens(token_response.access_token, refresh_token)
356363
return self._access_token, self._refresh_token
357364

358365
def revoke(self):
@@ -381,7 +388,7 @@ def revoke(self):
381388

382389
def downscope_token(self, scopes, item=None, additional_data=None):
383390
"""
384-
Get a downscoped token for the provided file or folder with the provided scopes.
391+
Generate a downscoped token for the provided file or folder with the provided scopes.
385392
386393
:param scope:
387394
The scope(s) to apply to the resulting token.
@@ -397,9 +404,9 @@ def downscope_token(self, scopes, item=None, additional_data=None):
397404
:type additional_data:
398405
`dict`
399406
:return:
400-
The downscoped token
407+
The response for the downscope token request.
401408
:rtype:
402-
`unicode`
409+
:class:`TokenResponse`
403410
"""
404411
self._check_closed()
405412
with self._refresh_lock:
@@ -416,8 +423,7 @@ def downscope_token(self, scopes, item=None, additional_data=None):
416423
if additional_data:
417424
data.update(additional_data)
418425

419-
access_token, _ = self._send_token_request_without_storing_tokens(data, access_token, expect_refresh_token=False)
420-
return access_token
426+
return self._execute_token_request(data, access_token, expect_refresh_token=False)
421427

422428
def close(self, revoke=True):
423429
"""Close the auth object.

boxsdk/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
from __future__ import unicode_literals, absolute_import
44

55

6-
__version__ = '2.0.0a8'
6+
__version__ = '2.0.0a9'

test/unit/auth/test_oauth2.py

Lines changed: 50 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,45 @@ def test_revoke_sends_revoke_request(
346346
assert oauth.access_token is None
347347

348348

349+
@pytest.fixture
350+
def check_downscope_token_request(
351+
oauth,
352+
mock_network_layer,
353+
mock_box_session,
354+
mock_object_id,
355+
make_mock_box_request,
356+
):
357+
def do_check(access_token, item_class, scopes, additional_data, expected_data):
358+
dummy_downscoped_token = 'dummy_downscoped_token'
359+
dummy_expires_in = 1234
360+
mock_network_response, _ = make_mock_box_request(
361+
response={'access_token': dummy_downscoped_token, 'expires_in': dummy_expires_in},
362+
)
363+
mock_network_layer.request.return_value = mock_network_response
364+
365+
item = item_class(mock_box_session, mock_object_id) if item_class else None
366+
367+
if additional_data:
368+
downscoped_token_response = oauth.downscope_token(scopes, item, additional_data)
369+
else:
370+
downscoped_token_response = oauth.downscope_token(scopes, item)
371+
372+
assert downscoped_token_response.access_token == dummy_downscoped_token
373+
assert downscoped_token_response.expires_in == dummy_expires_in
374+
375+
if item:
376+
expected_data['resource'] = item.get_url()
377+
mock_network_layer.request.assert_called_once_with(
378+
'POST',
379+
'{0}/token'.format(API.OAUTH2_API_URL),
380+
data=expected_data,
381+
headers={'content-type': 'application/x-www-form-urlencoded'},
382+
access_token=access_token,
383+
)
384+
385+
return do_check
386+
387+
349388
@pytest.mark.parametrize(
350389
'item_class,scopes,expected_scopes',
351390
[
@@ -356,72 +395,34 @@ def test_revoke_sends_revoke_request(
356395
],
357396
)
358397
def test_downscope_token_sends_downscope_request(
359-
oauth,
360398
access_token,
361-
mock_network_layer,
362-
mock_box_session,
363-
mock_object_id,
364-
make_mock_box_request,
399+
check_downscope_token_request,
365400
item_class,
366401
scopes,
367402
expected_scopes,
368403
):
369-
mock_downscoped_token = 'mock_downscoped_token'
370-
mock_network_response, _ = make_mock_box_request(response={'access_token': mock_downscoped_token})
371-
mock_network_layer.request.return_value = mock_network_response
372-
373-
item = item_class(mock_box_session, mock_object_id) if item_class else None
374-
downscoped_token = oauth.downscope_token(scopes, item)
375-
376-
assert downscoped_token == mock_downscoped_token
377404
expected_data = {
378405
'subject_token': access_token,
379406
'subject_token_type': 'urn:ietf:params:oauth:token-type:access_token',
380407
'scope': expected_scopes,
381408
'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
382409
}
383-
if item:
384-
expected_data['resource'] = item.get_url()
385-
mock_network_layer.request.assert_called_once_with(
386-
'POST',
387-
'{0}/token'.format(API.OAUTH2_API_URL),
388-
data=expected_data,
389-
headers={'content-type': 'application/x-www-form-urlencoded'},
390-
access_token=access_token,
391-
)
410+
check_downscope_token_request(access_token, item_class, scopes, {}, expected_data)
392411

393412

394413
def test_downscope_token_sends_downscope_request_with_additional_data(
395-
oauth,
396414
access_token,
397-
mock_network_layer,
398-
mock_box_session,
399-
mock_object_id,
400-
make_mock_box_request,
415+
check_downscope_token_request,
401416
):
402-
mock_downscoped_token = 'mock_downscoped_token'
403-
mock_network_response, _ = make_mock_box_request(response={'access_token': mock_downscoped_token})
404-
mock_network_layer.request.return_value = mock_network_response
405-
406-
item = File(mock_box_session, mock_object_id)
407417
additional_data = {'grant_type': 'new_grant_type', 'extra_data_key': 'extra_data_value'}
408-
downscoped_token = oauth.downscope_token([TokenScope.ITEM_READWRITE], item, additional_data)
409-
410-
assert downscoped_token == mock_downscoped_token
411-
mock_network_layer.request.assert_called_once_with(
412-
'POST',
413-
'{0}/token'.format(API.OAUTH2_API_URL),
414-
data={
415-
'subject_token': access_token,
416-
'subject_token_type': 'urn:ietf:params:oauth:token-type:access_token',
417-
'scope': 'item_readwrite',
418-
'resource': item.get_url(),
419-
'grant_type': 'new_grant_type',
420-
'extra_data_key': 'extra_data_value',
421-
},
422-
headers={'content-type': 'application/x-www-form-urlencoded'},
423-
access_token=access_token,
424-
)
418+
expected_data = {
419+
'subject_token': access_token,
420+
'subject_token_type': 'urn:ietf:params:oauth:token-type:access_token',
421+
'scope': 'item_readwrite',
422+
'grant_type': 'new_grant_type',
423+
'extra_data_key': 'extra_data_value',
424+
}
425+
check_downscope_token_request(access_token, File, [TokenScope.ITEM_READWRITE], additional_data, expected_data)
425426

426427

427428
def test_tokens_get_updated_after_noop_refresh(client_id, client_secret, access_token, new_access_token, refresh_token, mock_network_layer):

0 commit comments

Comments
 (0)