Skip to content

Commit 61f54c6

Browse files
authored
Merge pull request #178 from box/jwt_user
Refactors to JWTAuth
2 parents cc49214 + a7a11b0 commit 61f54c6

File tree

6 files changed

+402
-177
lines changed

6 files changed

+402
-177
lines changed

README.rst

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -323,12 +323,12 @@ These users can then be authenticated:
323323
ned_auth = JWTAuth(
324324
client_id='YOUR_CLIENT_ID',
325325
client_secret='YOUR_CLIENT_SECRET',
326-
enterprise_id='YOUR_ENTERPRISE_ID',
326+
user=ned_stark_user,
327327
jwt_key_id='YOUR_JWT_KEY_ID',
328328
rsa_private_key_file_sys_path='CERT.PEM',
329329
store_tokens=your_store_tokens_callback_method,
330330
)
331-
ned_auth.authenticate_app_user(ned_stark_user)
331+
ned_auth.authenticate_user()
332332
ned_client = Client(ned_auth)
333333
334334
Requests made with ``ned_client`` (or objects returned from ``ned_client``'s methods)
@@ -396,7 +396,7 @@ Customization
396396
Custom Subclasses
397397
~~~~~~~~~~~~~~~~~
398398

399-
Custom subclasses of any SDK object with an ``_item_type`` field can be defined:
399+
Custom object subclasses can be defined:
400400

401401
.. code-block:: pycon
402402
@@ -407,12 +407,13 @@ Custom subclasses of any SDK object with an ``_item_type`` field can be defined:
407407
pass
408408
409409
client = Client(oauth)
410+
client.translator.register('folder', MyFolderSubclass)
410411
folder = client.folder('0')
411412
412413
>>> print folder
413414
>>> <Box MyFolderSubclass - 0>
414415
415-
If a subclass of an SDK object with an ``_item_type`` field is defined, instances of this subclass will be
416+
If an object subclass is registered in this way, instances of this subclass will be
416417
returned from all SDK methods that previously returned an instance of the parent. See ``BaseAPIJSONObjectMeta``
417418
and ``Translator`` to see how the SDK performs dynamic lookups to determine return types.
418419

boxsdk/auth/jwt_auth.py

Lines changed: 110 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
from cryptography.hazmat.backends import default_backend
1010
from cryptography.hazmat.primitives import serialization
1111
import jwt
12+
from six import string_types, text_type
1213

1314
from .oauth2 import OAuth2
15+
from ..object.user import User
1416
from ..util.compat import total_seconds
1517

1618

@@ -28,14 +30,20 @@ def __init__(
2830
jwt_key_id,
2931
rsa_private_key_file_sys_path,
3032
rsa_private_key_passphrase=None,
33+
user=None,
3134
store_tokens=None,
3235
box_device_id='0',
3336
box_device_name='',
3437
access_token=None,
3538
network_layer=None,
3639
jwt_algorithm='RS256',
3740
):
38-
"""
41+
"""Extends baseclass method.
42+
43+
If both `enterprise_id` and `user` are non-`None`, the `user` takes
44+
precedence when `refresh()` is called. This can be overruled with a
45+
call to `authenticate_instance()`.
46+
3947
:param client_id:
4048
Box API key used for identifying the application the user is authenticating with.
4149
:type client_id:
@@ -46,8 +54,15 @@ def __init__(
4654
`unicode`
4755
:param enterprise_id:
4856
The ID of the Box Developer Edition enterprise.
57+
58+
May be `None`, if the caller knows that it will not be
59+
authenticating as an enterprise instance / service account.
60+
61+
If `user` is passed, this value is not used, unless
62+
`authenticate_instance()` is called to clear the user and
63+
authenticate as the enterprise instance.
4964
:type enterprise_id:
50-
`unicode`
65+
`unicode` or `None`
5166
:param jwt_key_id:
5267
Key ID for the JWT assertion.
5368
:type jwt_key_id:
@@ -60,6 +75,27 @@ def __init__(
6075
Passphrase used to unlock the private key. Do not pass a unicode string - this must be bytes.
6176
:type rsa_private_key_passphrase:
6277
`str` or None
78+
:param user:
79+
(optional) The user to authenticate, expressed as a Box User ID or
80+
as a :class:`User` instance.
81+
82+
This value is not required. But if it is provided, then the user
83+
will be auto-authenticated at the time of the first API call or
84+
when calling `authenticate_user()` without any arguments.
85+
86+
Should be `None` if the intention is to authenticate as the
87+
enterprise instance / service account. If both `enterprise_id` and
88+
`user` are non-`None`, the `user` takes precedense when `refresh()`
89+
is called.
90+
91+
May be one of this application's created App User. Depending on the
92+
configured User Access Level, may also be any other App User or
93+
Managed User in the enterprise.
94+
95+
<https://docs.box.com/docs/configuring-box-platform#section-3-enabling-app-auth-and-app-users>
96+
<https://docs.box.com/docs/authentication#section-choosing-an-authentication-type>
97+
:type user:
98+
`unicode` or :class:`User` or `None`
6399
:param store_tokens:
64100
Optional callback for getting access to tokens for storing them.
65101
:type store_tokens:
@@ -85,6 +121,7 @@ def __init__(
85121
:type jwt_algorithm:
86122
`unicode`
87123
"""
124+
user_id = self._normalize_user_id(user)
88125
super(JWTAuth, self).__init__(
89126
client_id,
90127
client_secret,
@@ -104,12 +141,12 @@ def __init__(
104141
self._enterprise_id = enterprise_id
105142
self._jwt_algorithm = jwt_algorithm
106143
self._jwt_key_id = jwt_key_id
107-
self._user_id = None
144+
self._user_id = user_id
108145

109146
def _auth_with_jwt(self, sub, sub_type):
110147
"""
111148
Get an access token for use with Box Developer Edition. Pass an enterprise ID to get an enterprise token
112-
(which can be used to provision/deprovision users), or a user ID to get an app user token.
149+
(which can be used to provision/deprovision users), or a user ID to get a user token.
113150
114151
:param sub:
115152
The enterprise ID or user ID to auth.
@@ -157,31 +194,92 @@ def _auth_with_jwt(self, sub, sub_type):
157194
data['box_device_name'] = self._box_device_name
158195
return self.send_token_request(data, access_token=None, expect_refresh_token=False)[0]
159196

160-
def authenticate_app_user(self, user):
197+
def authenticate_user(self, user=None):
161198
"""
162-
Get an access token for an App User (part of Box Developer Edition).
199+
Get an access token for a User.
200+
201+
May be one of this application's created App User. Depending on the
202+
configured User Access Level, may also be any other App User or Managed
203+
User in the enterprise.
204+
205+
<https://docs.box.com/docs/configuring-box-platform#section-3-enabling-app-auth-and-app-users>
206+
<https://docs.box.com/docs/authentication#section-choosing-an-authentication-type>
163207
164208
:param user:
165-
The user to authenticate.
209+
(optional) The user to authenticate, expressed as a Box User ID or
210+
as a :class:`User` instance.
211+
212+
If not given, then the most recently provided user ID, if
213+
available, will be used.
166214
:type user:
167-
:class:`User`
215+
`unicode` or :class:`User`
216+
:raises:
217+
:exc:`ValueError` if no user ID was passed and the object is not
218+
currently configured with one.
168219
:return:
169-
The access token for the app user.
220+
The access token for the user.
170221
:rtype:
171222
`unicode`
172223
"""
173-
sub = self._user_id = user.object_id
224+
sub = self._normalize_user_id(user) or self._user_id
225+
if not sub:
226+
raise ValueError("authenticate_user: Requires the user ID, but it was not provided.")
227+
self._user_id = sub
174228
return self._auth_with_jwt(sub, 'user')
175229

176-
def authenticate_instance(self):
230+
authenticate_app_user = authenticate_user
231+
232+
@classmethod
233+
def _normalize_user_id(cls, user):
234+
"""Get a Box user ID from a selection of supported param types.
235+
236+
:param user:
237+
An object representing the user or user ID.
238+
239+
Currently supported types are `unicode` (which represents the user
240+
ID) and :class:`User`.
241+
242+
If `None`, returns `None`.
243+
:raises: :exc:`TypeError` for unsupported types.
244+
:rtype: `unicode` or `None`
245+
"""
246+
if user is None:
247+
return None
248+
if isinstance(user, User):
249+
return user.object_id
250+
if isinstance(user, string_types):
251+
return text_type(user)
252+
raise TypeError("Got unsupported type {0!r} for user.".format(user.__class__.__name__))
253+
254+
def authenticate_instance(self, enterprise=None):
177255
"""
178256
Get an access token for a Box Developer Edition enterprise.
179257
258+
:param enterprise:
259+
The ID of the Box Developer Edition enterprise.
260+
261+
Optional if the value was already given to `__init__`,
262+
otherwise required.
263+
:type enterprise: `unicode` or `None`
264+
:raises:
265+
:exc:`ValueError` if `None` was passed for the enterprise ID here
266+
and in `__init__`, or if the non-`None` value passed here does not
267+
match the non-`None` value passed to `__init__`.
180268
:return:
181269
The access token for the enterprise which can provision/deprovision app users.
182270
:rtype:
183271
`unicode`
184272
"""
273+
enterprises = [enterprise, self._enterprise_id]
274+
if not any(enterprises):
275+
raise ValueError("authenticate_instance: Requires the enterprise ID, but it was not provided.")
276+
if all(enterprises) and (enterprise != self._enterprise_id):
277+
raise ValueError(
278+
"authenticate_instance: Given enterprise ID {given_enterprise!r}, but {auth} already has ID {existing_enterprise!r}"
279+
.format(auth=self, given_enterprise=enterprise, existing_enterprise=self._enterprise_id)
280+
)
281+
if not self._enterprise_id:
282+
self._enterprise_id = enterprise
185283
self._user_id = None
186284
return self._auth_with_jwt(self._enterprise_id, 'enterprise')
187285

@@ -195,4 +293,4 @@ def _refresh(self, access_token):
195293
if self._user_id is None:
196294
return self.authenticate_instance()
197295
else:
198-
return self._auth_with_jwt(self._user_id, 'user')
296+
return self.authenticate_user()

boxsdk/session/box_session.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,8 @@ def _renew_session(self, access_token_used):
193193
:type access_token_used:
194194
`unicode`
195195
"""
196-
self._oauth.refresh(access_token_used)
196+
new_access_token, _ = self._oauth.refresh(access_token_used)
197+
return new_access_token
197198

198199
@staticmethod
199200
def _is_json_response(network_response):
@@ -390,6 +391,9 @@ def _make_request(
390391
# Since there can be session renewal happening in the middle of preparing the request, it's important to be
391392
# consistent with the access_token being used in the request.
392393
access_token_will_be_used = self._oauth.access_token
394+
if auto_session_renewal and (access_token_will_be_used is None):
395+
access_token_will_be_used = self._renew_session(None)
396+
auto_session_renewal = False
393397
authorization_header = {'Authorization': 'Bearer {0}'.format(access_token_will_be_used)}
394398
if headers is None:
395399
headers = self._default_headers.copy()

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ pyjwt>=1.3.0
44
requests>=2.4.3
55
requests-toolbelt>=0.4.0
66
six >= 1.4.0
7-
-e .
7+
-e .[all]

0 commit comments

Comments
 (0)