Skip to content

Commit

Permalink
Change downscope_token() to return a TokenResponse object (#237)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
anthonywee authored and jmoldow committed Sep 19, 2017
1 parent e65d34c commit b7f0bfd
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 68 deletions.
42 changes: 24 additions & 18 deletions boxsdk/auth/oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
import six
from six.moves.urllib.parse import urlencode, urlunsplit # pylint:disable=import-error,no-name-in-module

from boxsdk.network.default_network import DefaultNetwork
from boxsdk.config import API
from boxsdk.exception import BoxOAuthException
from boxsdk.network.default_network import DefaultNetwork
from boxsdk.object.base_api_json_object import BaseAPIJSONObject
from boxsdk.util.text_enum import TextEnum


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


class TokenResponse(BaseAPIJSONObject):
""" Represents the response for a token request. """
pass


class OAuth2(object):
"""
Responsible for handling OAuth2 for the Box API. Can authenticate and refresh tokens.
Expand Down Expand Up @@ -294,7 +300,7 @@ def _update_current_tokens(self, access_token, refresh_token):
"""
self._access_token, self._refresh_token = access_token, refresh_token

def _send_token_request_without_storing_tokens(self, data, access_token, expect_refresh_token=True):
def _execute_token_request(self, data, access_token, expect_refresh_token=True):
"""
Send the request to acquire or refresh an access token.
Expand All @@ -307,9 +313,9 @@ def _send_token_request_without_storing_tokens(self, data, access_token, expect_
:type access_token:
`unicode` or None
:return:
The access token and refresh token.
The response for the token request.
:rtype:
(`unicode`, `unicode`)
:class:`TokenResponse`
"""
self._check_closed()
url = '{base_auth_url}/token'.format(base_auth_url=API.OAUTH2_API_URL)
Expand All @@ -324,15 +330,14 @@ def _send_token_request_without_storing_tokens(self, data, access_token, expect_
if not network_response.ok:
raise BoxOAuthException(network_response.status_code, network_response.content, url, 'POST')
try:
response = network_response.json()
access_token = response['access_token']
refresh_token = response.get('refresh_token', None)
if refresh_token is None and expect_refresh_token:
raise BoxOAuthException(network_response.status_code, network_response.content, url, 'POST')
except (ValueError, KeyError):
token_response = TokenResponse(network_response.json())
except ValueError:
raise BoxOAuthException(network_response.status_code, network_response.content, url, 'POST')

return access_token, refresh_token
if ('access_token' not in token_response) or (expect_refresh_token and 'refresh_token' not in token_response):
raise BoxOAuthException(network_response.status_code, network_response.content, url, 'POST')

return token_response

def send_token_request(self, data, access_token, expect_refresh_token=True):
"""
Expand All @@ -351,8 +356,10 @@ def send_token_request(self, data, access_token, expect_refresh_token=True):
:rtype:
(`unicode`, `unicode`)
"""
access_token, refresh_token = self._send_token_request_without_storing_tokens(data, access_token, expect_refresh_token)
self._store_tokens(access_token, refresh_token)
token_response = self._execute_token_request(data, access_token, expect_refresh_token)
# pylint:disable=no-member
refresh_token = token_response.refresh_token if 'refresh_token' in token_response else None
self._store_tokens(token_response.access_token, refresh_token)
return self._access_token, self._refresh_token

def revoke(self):
Expand Down Expand Up @@ -381,7 +388,7 @@ def revoke(self):

def downscope_token(self, scopes, item=None, additional_data=None):
"""
Get a downscoped token for the provided file or folder with the provided scopes.
Generate a downscoped token for the provided file or folder with the provided scopes.
:param scope:
The scope(s) to apply to the resulting token.
Expand All @@ -397,9 +404,9 @@ def downscope_token(self, scopes, item=None, additional_data=None):
:type additional_data:
`dict`
:return:
The downscoped token
The response for the downscope token request.
:rtype:
`unicode`
:class:`TokenResponse`
"""
self._check_closed()
with self._refresh_lock:
Expand All @@ -416,8 +423,7 @@ def downscope_token(self, scopes, item=None, additional_data=None):
if additional_data:
data.update(additional_data)

access_token, _ = self._send_token_request_without_storing_tokens(data, access_token, expect_refresh_token=False)
return access_token
return self._execute_token_request(data, access_token, expect_refresh_token=False)

def close(self, revoke=True):
"""Close the auth object.
Expand Down
2 changes: 1 addition & 1 deletion boxsdk/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
from __future__ import unicode_literals, absolute_import


__version__ = '2.0.0a8'
__version__ = '2.0.0a9'
99 changes: 50 additions & 49 deletions test/unit/auth/test_oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,45 @@ def test_revoke_sends_revoke_request(
assert oauth.access_token is None


@pytest.fixture
def check_downscope_token_request(
oauth,
mock_network_layer,
mock_box_session,
mock_object_id,
make_mock_box_request,
):
def do_check(access_token, item_class, scopes, additional_data, expected_data):
dummy_downscoped_token = 'dummy_downscoped_token'
dummy_expires_in = 1234
mock_network_response, _ = make_mock_box_request(
response={'access_token': dummy_downscoped_token, 'expires_in': dummy_expires_in},
)
mock_network_layer.request.return_value = mock_network_response

item = item_class(mock_box_session, mock_object_id) if item_class else None

if additional_data:
downscoped_token_response = oauth.downscope_token(scopes, item, additional_data)
else:
downscoped_token_response = oauth.downscope_token(scopes, item)

assert downscoped_token_response.access_token == dummy_downscoped_token
assert downscoped_token_response.expires_in == dummy_expires_in

if item:
expected_data['resource'] = item.get_url()
mock_network_layer.request.assert_called_once_with(
'POST',
'{0}/token'.format(API.OAUTH2_API_URL),
data=expected_data,
headers={'content-type': 'application/x-www-form-urlencoded'},
access_token=access_token,
)

return do_check


@pytest.mark.parametrize(
'item_class,scopes,expected_scopes',
[
Expand All @@ -356,72 +395,34 @@ def test_revoke_sends_revoke_request(
],
)
def test_downscope_token_sends_downscope_request(
oauth,
access_token,
mock_network_layer,
mock_box_session,
mock_object_id,
make_mock_box_request,
check_downscope_token_request,
item_class,
scopes,
expected_scopes,
):
mock_downscoped_token = 'mock_downscoped_token'
mock_network_response, _ = make_mock_box_request(response={'access_token': mock_downscoped_token})
mock_network_layer.request.return_value = mock_network_response

item = item_class(mock_box_session, mock_object_id) if item_class else None
downscoped_token = oauth.downscope_token(scopes, item)

assert downscoped_token == mock_downscoped_token
expected_data = {
'subject_token': access_token,
'subject_token_type': 'urn:ietf:params:oauth:token-type:access_token',
'scope': expected_scopes,
'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
}
if item:
expected_data['resource'] = item.get_url()
mock_network_layer.request.assert_called_once_with(
'POST',
'{0}/token'.format(API.OAUTH2_API_URL),
data=expected_data,
headers={'content-type': 'application/x-www-form-urlencoded'},
access_token=access_token,
)
check_downscope_token_request(access_token, item_class, scopes, {}, expected_data)


def test_downscope_token_sends_downscope_request_with_additional_data(
oauth,
access_token,
mock_network_layer,
mock_box_session,
mock_object_id,
make_mock_box_request,
check_downscope_token_request,
):
mock_downscoped_token = 'mock_downscoped_token'
mock_network_response, _ = make_mock_box_request(response={'access_token': mock_downscoped_token})
mock_network_layer.request.return_value = mock_network_response

item = File(mock_box_session, mock_object_id)
additional_data = {'grant_type': 'new_grant_type', 'extra_data_key': 'extra_data_value'}
downscoped_token = oauth.downscope_token([TokenScope.ITEM_READWRITE], item, additional_data)

assert downscoped_token == mock_downscoped_token
mock_network_layer.request.assert_called_once_with(
'POST',
'{0}/token'.format(API.OAUTH2_API_URL),
data={
'subject_token': access_token,
'subject_token_type': 'urn:ietf:params:oauth:token-type:access_token',
'scope': 'item_readwrite',
'resource': item.get_url(),
'grant_type': 'new_grant_type',
'extra_data_key': 'extra_data_value',
},
headers={'content-type': 'application/x-www-form-urlencoded'},
access_token=access_token,
)
expected_data = {
'subject_token': access_token,
'subject_token_type': 'urn:ietf:params:oauth:token-type:access_token',
'scope': 'item_readwrite',
'grant_type': 'new_grant_type',
'extra_data_key': 'extra_data_value',
}
check_downscope_token_request(access_token, File, [TokenScope.ITEM_READWRITE], additional_data, expected_data)


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

0 comments on commit b7f0bfd

Please sign in to comment.