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
9 changes: 5 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -323,12 +323,12 @@ These users can then be authenticated:
ned_auth = JWTAuth(
client_id='YOUR_CLIENT_ID',
client_secret='YOUR_CLIENT_SECRET',
enterprise_id='YOUR_ENTERPRISE_ID',
user=ned_stark_user,
jwt_key_id='YOUR_JWT_KEY_ID',
rsa_private_key_file_sys_path='CERT.PEM',
store_tokens=your_store_tokens_callback_method,
)
ned_auth.authenticate_app_user(ned_stark_user)
ned_auth.authenticate_user()
ned_client = Client(ned_auth)

Requests made with ``ned_client`` (or objects returned from ``ned_client``'s methods)
Expand Down Expand Up @@ -396,7 +396,7 @@ Customization
Custom Subclasses
~~~~~~~~~~~~~~~~~

Custom subclasses of any SDK object with an ``_item_type`` field can be defined:
Custom object subclasses can be defined:

.. code-block:: pycon

Expand All @@ -407,12 +407,13 @@ Custom subclasses of any SDK object with an ``_item_type`` field can be defined:
pass

client = Client(oauth)
client.translator.register('folder', MyFolderSubclass)
folder = client.folder('0')

>>> print folder
>>> <Box MyFolderSubclass - 0>

If a subclass of an SDK object with an ``_item_type`` field is defined, instances of this subclass will be
If an object subclass is registered in this way, instances of this subclass will be
returned from all SDK methods that previously returned an instance of the parent. See ``BaseAPIJSONObjectMeta``
and ``Translator`` to see how the SDK performs dynamic lookups to determine return types.

Expand Down
122 changes: 110 additions & 12 deletions boxsdk/auth/jwt_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import jwt
from six import string_types, text_type

from .oauth2 import OAuth2
from ..object.user import User
from ..util.compat import total_seconds


Expand All @@ -28,14 +30,20 @@ def __init__(
jwt_key_id,
rsa_private_key_file_sys_path,
rsa_private_key_passphrase=None,
user=None,
store_tokens=None,
box_device_id='0',
box_device_name='',
access_token=None,
network_layer=None,
jwt_algorithm='RS256',
):
"""
"""Extends baseclass method.

If both `enterprise_id` and `user` are non-`None`, the `user` takes
precedence when `refresh()` is called. This can be overruled with a
call to `authenticate_instance()`.

:param client_id:
Box API key used for identifying the application the user is authenticating with.
:type client_id:
Expand All @@ -46,8 +54,15 @@ def __init__(
`unicode`
:param enterprise_id:
The ID of the Box Developer Edition enterprise.

May be `None`, if the caller knows that it will not be
authenticating as an enterprise instance / service account.

If `user` is passed, this value is not used, unless
`authenticate_instance()` is called to clear the user and
authenticate as the enterprise instance.
:type enterprise_id:
`unicode`
`unicode` or `None`
:param jwt_key_id:
Key ID for the JWT assertion.
:type jwt_key_id:
Expand All @@ -60,6 +75,27 @@ def __init__(
Passphrase used to unlock the private key. Do not pass a unicode string - this must be bytes.
:type rsa_private_key_passphrase:
`str` or None
:param user:
(optional) The user to authenticate, expressed as a Box User ID or
as a :class:`User` instance.

This value is not required. But if it is provided, then the user
will be auto-authenticated at the time of the first API call or
when calling `authenticate_user()` without any arguments.

Should be `None` if the intention is to authenticate as the
enterprise instance / service account. If both `enterprise_id` and
`user` are non-`None`, the `user` takes precedense when `refresh()`
is called.

May be one of this application's created App User. Depending on the
configured User Access Level, may also be any other App User or
Managed User in the enterprise.

<https://docs.box.com/docs/configuring-box-platform#section-3-enabling-app-auth-and-app-users>
<https://docs.box.com/docs/authentication#section-choosing-an-authentication-type>
:type user:
`unicode` or :class:`User` or `None`
:param store_tokens:
Optional callback for getting access to tokens for storing them.
:type store_tokens:
Expand All @@ -85,6 +121,7 @@ def __init__(
:type jwt_algorithm:
`unicode`
"""
user_id = self._normalize_user_id(user)
super(JWTAuth, self).__init__(
client_id,
client_secret,
Expand All @@ -104,12 +141,12 @@ def __init__(
self._enterprise_id = enterprise_id
self._jwt_algorithm = jwt_algorithm
self._jwt_key_id = jwt_key_id
self._user_id = None
self._user_id = user_id

def _auth_with_jwt(self, sub, sub_type):
"""
Get an access token for use with Box Developer Edition. Pass an enterprise ID to get an enterprise token
(which can be used to provision/deprovision users), or a user ID to get an app user token.
(which can be used to provision/deprovision users), or a user ID to get a user token.

:param sub:
The enterprise ID or user ID to auth.
Expand Down Expand Up @@ -157,31 +194,92 @@ def _auth_with_jwt(self, sub, sub_type):
data['box_device_name'] = self._box_device_name
return self.send_token_request(data, access_token=None, expect_refresh_token=False)[0]

def authenticate_app_user(self, user):
def authenticate_user(self, user=None):
"""
Get an access token for an App User (part of Box Developer Edition).
Get an access token for a User.

May be one of this application's created App User. Depending on the
configured User Access Level, may also be any other App User or Managed
User in the enterprise.

<https://docs.box.com/docs/configuring-box-platform#section-3-enabling-app-auth-and-app-users>
<https://docs.box.com/docs/authentication#section-choosing-an-authentication-type>

:param user:
The user to authenticate.
(optional) The user to authenticate, expressed as a Box User ID or
as a :class:`User` instance.

If not given, then the most recently provided user ID, if
available, will be used.
:type user:
:class:`User`
`unicode` or :class:`User`
:raises:
:exc:`ValueError` if no user ID was passed and the object is not
currently configured with one.
:return:
The access token for the app user.
The access token for the user.
:rtype:
`unicode`
"""
sub = self._user_id = user.object_id
sub = self._normalize_user_id(user) or self._user_id
if not sub:
raise ValueError("authenticate_user: Requires the user ID, but it was not provided.")
self._user_id = sub
return self._auth_with_jwt(sub, 'user')

def authenticate_instance(self):
authenticate_app_user = authenticate_user

@classmethod
def _normalize_user_id(cls, user):
"""Get a Box user ID from a selection of supported param types.

:param user:
An object representing the user or user ID.

Currently supported types are `unicode` (which represents the user
ID) and :class:`User`.

If `None`, returns `None`.
:raises: :exc:`TypeError` for unsupported types.
:rtype: `unicode` or `None`
"""
if user is None:
return None
if isinstance(user, User):
return user.object_id
if isinstance(user, string_types):
return text_type(user)
raise TypeError("Got unsupported type {0!r} for user.".format(user.__class__.__name__))

def authenticate_instance(self, enterprise=None):
"""
Get an access token for a Box Developer Edition enterprise.

:param enterprise:
The ID of the Box Developer Edition enterprise.

Optional if the value was already given to `__init__`,
otherwise required.
:type enterprise: `unicode` or `None`
:raises:
:exc:`ValueError` if `None` was passed for the enterprise ID here
and in `__init__`, or if the non-`None` value passed here does not
match the non-`None` value passed to `__init__`.
:return:
The access token for the enterprise which can provision/deprovision app users.
:rtype:
`unicode`
"""
enterprises = [enterprise, self._enterprise_id]
if not any(enterprises):
raise ValueError("authenticate_instance: Requires the enterprise ID, but it was not provided.")
if all(enterprises) and (enterprise != self._enterprise_id):
raise ValueError(
"authenticate_instance: Given enterprise ID {given_enterprise!r}, but {auth} already has ID {existing_enterprise!r}"
.format(auth=self, given_enterprise=enterprise, existing_enterprise=self._enterprise_id)
)
if not self._enterprise_id:
self._enterprise_id = enterprise
self._user_id = None
return self._auth_with_jwt(self._enterprise_id, 'enterprise')

Expand All @@ -195,4 +293,4 @@ def _refresh(self, access_token):
if self._user_id is None:
return self.authenticate_instance()
else:
return self._auth_with_jwt(self._user_id, 'user')
return self.authenticate_user()
6 changes: 5 additions & 1 deletion boxsdk/session/box_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ def _renew_session(self, access_token_used):
:type access_token_used:
`unicode`
"""
self._oauth.refresh(access_token_used)
new_access_token, _ = self._oauth.refresh(access_token_used)
return new_access_token

@staticmethod
def _is_json_response(network_response):
Expand Down Expand Up @@ -390,6 +391,9 @@ def _make_request(
# Since there can be session renewal happening in the middle of preparing the request, it's important to be
# consistent with the access_token being used in the request.
access_token_will_be_used = self._oauth.access_token
if auto_session_renewal and (access_token_will_be_used is None):
access_token_will_be_used = self._renew_session(None)
auto_session_renewal = False
authorization_header = {'Authorization': 'Bearer {0}'.format(access_token_will_be_used)}
if headers is None:
headers = self._default_headers.copy()
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ pyjwt>=1.3.0
requests>=2.4.3
requests-toolbelt>=0.4.0
six >= 1.4.0
-e .
-e .[all]
Loading