Skip to content

Commit 12fb081

Browse files
committed
Refactors to JWTAuth
Rename `JWTAuth.authenticate_app_user()` to `JWTAuth.authenticate_user()`, to reflect the fact that the new Box service accounts can authenticate as managed users as well as app users. `JWTAuth.authenticate_app_user()` is retained as an alias, for backwards-compatability. Allow a user ID to be passed to `JWTAuth`, instead of a `User` object. Previously, developers would need to import and manually construct a `boxsdk.object.user.User`, since `Client.user()` cannot be used until the `Client` has been constructed (which is usually done after all authentication is complete). Allow a user or user ID to be passed to the `JWTAuth` constructor. This way, `JWTAuth.authenticate_user()` can be called without any arguments. More importantly, this means that `JWTAuth.refresh()` can be called immediately after construction, with no need for a manual call to `JWTAuth.authenticate_user()`. When `auto_session_renewal=True`, if there is no access token, then `BoxSession.request()` will renew the token _before_ making the request. This saves an API call. And combined with the above, authentication for `JWTAuth` objects can be done completely automatically, at the time of first API call. Document that the `enterprise_id` argument to `JWTAuth` is allowed to be `None` (and must be `None` if `user` is passed). Also allow it to be passed to `authenticate_instance()`, so that it doesn't need to be provided at construction time. Clean up the README. Fixes #174.
1 parent 8d0a165 commit 12fb081

File tree

6 files changed

+408
-177
lines changed

6 files changed

+408
-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: 108 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,21 @@ 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+
At most one of `enterprise_id` and `user` may be non-`None`. If they
44+
are both `None`, then the caller is required to manually call either
45+
`authenticate_user()` or `authenticate_instance()` with the desired
46+
`sub` claim in order to begin using this auth object.
47+
3948
:param client_id:
4049
Box API key used for identifying the application the user is authenticating with.
4150
:type client_id:
@@ -46,8 +55,13 @@ def __init__(
4655
`unicode`
4756
:param enterprise_id:
4857
The ID of the Box Developer Edition enterprise.
58+
59+
May be `None`, if the caller knows that it will not be
60+
authenticating as an enterprise instance / service account.
61+
62+
Must be `None` if `user` is passed.
4963
:type enterprise_id:
50-
`unicode`
64+
`unicode` or `None`
5165
:param jwt_key_id:
5266
Key ID for the JWT assertion.
5367
:type jwt_key_id:
@@ -60,6 +74,24 @@ def __init__(
6074
Passphrase used to unlock the private key. Do not pass a unicode string - this must be bytes.
6175
:type rsa_private_key_passphrase:
6276
`str` or None
77+
:param user:
78+
(optional) The user to authenticate, expressed as a Box User ID or
79+
as a :class:`User` instance.
80+
81+
This value is not required. But if it is provided, then the user
82+
will be auto-authenticated at the time of the first API call or
83+
when calling `authenticate_user()` without any arguments.
84+
85+
Must be `None` if `enterprise_id` is passed.
86+
87+
May be one of this application's created App User. Depending on the
88+
configured User Access Level, may also be any other App User or
89+
Managed User in the enterprise.
90+
91+
<https://docs.box.com/docs/configuring-box-platform#section-3-enabling-app-auth-and-app-users>
92+
<https://docs.box.com/docs/authentication#section-choosing-an-authentication-type>
93+
:type user:
94+
`unicode` or :class:`User` or `None`
6395
:param store_tokens:
6496
Optional callback for getting access to tokens for storing them.
6597
:type store_tokens:
@@ -85,6 +117,9 @@ def __init__(
85117
:type jwt_algorithm:
86118
`unicode`
87119
"""
120+
if enterprise_id and user:
121+
raise TypeError("Cannot pass both 'enterprise_id' and 'user'.")
122+
user_id = self._normalize_user_id(user)
88123
super(JWTAuth, self).__init__(
89124
client_id,
90125
client_secret,
@@ -104,12 +139,12 @@ def __init__(
104139
self._enterprise_id = enterprise_id
105140
self._jwt_algorithm = jwt_algorithm
106141
self._jwt_key_id = jwt_key_id
107-
self._user_id = None
142+
self._user_id = user_id
108143

109144
def _auth_with_jwt(self, sub, sub_type):
110145
"""
111146
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.
147+
(which can be used to provision/deprovision users), or a user ID to get a user token.
113148
114149
:param sub:
115150
The enterprise ID or user ID to auth.
@@ -157,31 +192,92 @@ def _auth_with_jwt(self, sub, sub_type):
157192
data['box_device_name'] = self._box_device_name
158193
return self.send_token_request(data, access_token=None, expect_refresh_token=False)[0]
159194

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

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

@@ -195,4 +291,4 @@ def _refresh(self, access_token):
195291
if self._user_id is None:
196292
return self.authenticate_instance()
197293
else:
198-
return self._auth_with_jwt(self._user_id, 'user')
294+
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)