Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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