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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ __pycache__/
*.pyc
*.pyo
*~
.venv
.venv
venv/
venv3/
34 changes: 27 additions & 7 deletions dropbox/dropbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,11 @@ def __init__(self,
oauth2_refresh_token=None,
oauth2_access_token_expiration=None,
app_key=None,
app_secret=None):
app_secret=None,
scope=None,):
"""
:param str oauth2_access_token: OAuth2 access token for making client
requests.
:param str oauth2_refresh_token: OAuth2 refresh token for refreshing access token
:param datetime oauth2_access_token_expiration: Expiration for oauth2_access_token
:param int max_retries_on_error: On 5xx errors, the number of times to
retry.
:param Optional[int] max_retries_on_rate_limit: On 429 errors, the
Expand All @@ -165,6 +164,12 @@ def __init__(self,
server. After the timeout the client will give up on
connection. If `None`, client will wait forever. Defaults
to 30 seconds.
:param str oauth2_refresh_token: OAuth2 refresh token for refreshing access token
:param datetime oauth2_access_token_expiration: Expiration for oauth2_access_token
:param str app_key: application key of requesting application; used for token refresh
:param str app_secret: application secret of requesting application; used for token refresh
:param list scope: list of scopes to request on refresh. If left blank,
refresh will request all available scopes for application
"""

assert oauth2_access_token or oauth2_refresh_token, \
Expand All @@ -177,12 +182,17 @@ def __init__(self,
assert app_key and app_secret, \
"app_key and app_secret are required to refresh tokens"

if scope is not None:
assert len(scope) > 0 and isinstance(scope, list), \
"Scope list must be of type list"

self._oauth2_access_token = oauth2_access_token
self._oauth2_refresh_token = oauth2_refresh_token
self._oauth2_access_token_expiration = oauth2_access_token_expiration

self._app_key = app_key
self._app_secret = app_secret
self._scope = scope

self._max_retries_on_error = max_retries_on_error
self._max_retries_on_rate_limit = max_retries_on_rate_limit
Expand Down Expand Up @@ -222,7 +232,8 @@ def clone(
oauth2_refresh_token=None,
oauth2_access_token_expiration=None,
app_key=None,
app_secret=None):
app_secret=None,
scope=None):
"""
Creates a new copy of the Dropbox client with the same defaults unless modified by
arguments to clone()
Expand All @@ -245,6 +256,7 @@ def clone(
oauth2_access_token_expiration or self._oauth2_access_token_expiration,
app_key or self._app_key,
app_secret or self._app_secret,
scope or self._scope
)

def request(self,
Expand Down Expand Up @@ -332,7 +344,6 @@ def request(self,
def check_and_refresh_access_token(self):
"""
Checks if access token needs to be refreshed and refreshes if possible

:return:
"""
can_refresh = self._oauth2_refresh_token and self._app_key and self._app_secret
Expand All @@ -341,15 +352,21 @@ def check_and_refresh_access_token(self):
self._oauth2_access_token_expiration
needs_token = not self._oauth2_access_token
if (needs_refresh or needs_token) and can_refresh:
self.refresh_access_token()
self.refresh_access_token(scope=self._scope)

def refresh_access_token(self, host=API_HOST):
def refresh_access_token(self, host=API_HOST, scope=None):
"""
Refreshes an access token via refresh token if available

:param host: host to hit token endpoint with
:param scope: list of permission scopes for access token
:return:
"""

if scope is not None:
assert len(scope) > 0 and isinstance(scope, list), \
"Scope list must be of type list"

if not (self._oauth2_refresh_token and self._app_key and self._app_secret):
self._logger.warning('Unable to refresh access token without \
refresh token, app key, and app secret')
Expand All @@ -362,6 +379,9 @@ def refresh_access_token(self, host=API_HOST):
'client_id': self._app_key,
'client_secret': self._app_secret,
}
if scope:
scope = " ".join(scope)
body['scope'] = scope

res = self._session.post(url, data=body)
if res.status_code == 400 and res.json()['error'] == 'invalid_grant':
Expand Down
91 changes: 72 additions & 19 deletions dropbox/oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
url_encode = urllib.urlencode # pylint: disable=no-member,useless-suppression

TOKEN_ACCESS_TYPES = ['offline', 'online', 'legacy']
INCLUDE_GRANTED_SCOPES_TYPES = ['user', 'team']

class OAuth2FlowNoRedirectResult(object):
"""
Expand All @@ -39,17 +40,22 @@ class OAuth2FlowNoRedirectResult(object):
in using them, please contact Dropbox support
"""

def __init__(self, access_token, account_id, user_id, refresh_token, expiration):
def __init__(self, access_token, account_id, user_id, refresh_token, expiration, scope):
"""
Args:
access_token (str): Token to be used to authenticate later
requests.
refresh_token (str): Token to be used to acquire new access token
when existing one expires
expiration (int, datetime): Either the number of seconds from now that the token expires
in or the datetime at which the token expires
account_id (str): The Dropbox user's account ID.
user_id (str): Deprecated (use account_id instead).
refresh_token (str): Token to be used to acquire new access token
when existing one expires
expiration (int, datetime): Either the number of seconds from now that the token expires
in or the datetime at which the token expires
scope (list): list of scopes to request in base oauth flow.
"""
self.access_token = access_token
if not expiration:
Expand All @@ -61,14 +67,16 @@ def __init__(self, access_token, account_id, user_id, refresh_token, expiration)
self.refresh_token = refresh_token
self.account_id = account_id
self.user_id = user_id
self.scope = scope

def __repr__(self):
return 'OAuth2FlowNoRedirectResult(%s, %s, %s, %s, %s)' % (
return 'OAuth2FlowNoRedirectResult(%s, %s, %s, %s, %s, %s)' % (
self.access_token,
self.account_id,
self.user_id,
self.refresh_token,
self.expires_at,
self.scope,
)


Expand All @@ -77,7 +85,8 @@ class OAuth2FlowResult(OAuth2FlowNoRedirectResult):
Authorization information for an OAuth2Flow with redirect.
"""

def __init__(self, access_token, account_id, user_id, url_state, refresh_token, expires_in):
def __init__(self, access_token, account_id, user_id, url_state, refresh_token,
expires_in, scope):
"""
Same as OAuth2FlowNoRedirectResult but with url_state.

Expand All @@ -86,20 +95,23 @@ def __init__(self, access_token, account_id, user_id, url_state, refresh_token,
:meth:`DropboxOAuth2Flow.start`.
"""
super(OAuth2FlowResult, self).__init__(
access_token, account_id, user_id, refresh_token, expires_in)
access_token, account_id, user_id, refresh_token, expires_in, scope)
self.url_state = url_state

@classmethod
def from_no_redirect_result(cls, result, url_state):
assert isinstance(result, OAuth2FlowNoRedirectResult)
return cls(result.access_token, result.account_id, result.user_id,
url_state, result.refresh_token, result.expires_at)
url_state, result.refresh_token, result.expires_at, result.scope)

def __repr__(self):
return 'OAuth2FlowResult(%s, %s, %s, %s, %s, %s)' % (
return 'OAuth2FlowResult(%s, %s, %s, %s, %s, %s, %s, %s, %s)' % (
self.access_token,
self.refresh_token,
self.expires_at,
self.account_id,
self.user_id,
self.scope,
self.url_state,
self.refresh_token,
self.expires_at,
Expand All @@ -108,14 +120,22 @@ def __repr__(self):

class DropboxOAuth2FlowBase(object):

def __init__(self, consumer_key, consumer_secret, locale=None, token_access_type='legacy'):
def __init__(self, consumer_key, consumer_secret, locale=None, token_access_type='legacy',
scope=None, include_granted_scopes=None):
if scope is not None:
assert len(scope) > 0 and isinstance(scope, list), \
"Scope list must be of type list"

self.consumer_key = consumer_key
self.consumer_secret = consumer_secret
self.locale = locale
self.token_access_type = token_access_type
self.requests_session = pinned_session()
self.scope = scope
self.include_granted_scopes = include_granted_scopes

def _get_authorize_url(self, redirect_uri, state, token_access_type):
def _get_authorize_url(self, redirect_uri, state, token_access_type, scope=None,
include_granted_scopes=None):
params = dict(response_type='code',
client_id=self.consumer_key)
if redirect_uri is not None:
Expand All @@ -127,6 +147,12 @@ def _get_authorize_url(self, redirect_uri, state, token_access_type):
if token_access_type != 'legacy':
params['token_access_type'] = token_access_type

if scope is not None:
params['scope'] = " ".join(scope)
if include_granted_scopes is not None:
assert include_granted_scopes in INCLUDE_GRANTED_SCOPES_TYPES
params['include_granted_scopes'] = str(include_granted_scopes)

return self.build_url('/oauth2/authorize', params, WEB_HOST)

def _finish(self, code, redirect_uri):
Expand Down Expand Up @@ -163,6 +189,11 @@ def _finish(self, code, redirect_uri):
else:
expires_in = None

if 'scope' in d:
scope = d['scope']
else:
scope = None

uid = d['uid']

return OAuth2FlowNoRedirectResult(
Expand All @@ -171,7 +202,7 @@ def _finish(self, code, redirect_uri):
uid,
refresh_token,
expires_in,
)
scope)

def build_path(self, target, params=None):
"""Build the path component for an API URL.
Expand Down Expand Up @@ -228,21 +259,23 @@ class DropboxOAuth2FlowNoRedirect(DropboxOAuth2FlowBase):
auth_flow = DropboxOAuth2FlowNoRedirect(APP_KEY, APP_SECRET)

authorize_url = auth_flow.start()
print "1. Go to: " + authorize_url
print "2. Click \\"Allow\\" (you might have to log in first)."
print "3. Copy the authorization code."
print("1. Go to: " + authorize_url)
print("2. Click \\"Allow\\" (you might have to log in first).")
print("3. Copy the authorization code.")
auth_code = raw_input("Enter the authorization code here: ").strip()

try:
oauth_result = auth_flow.finish(auth_code)
except Exception, e:
except Exception as e:
print('Error: %s' % (e,))
return

dbx = Dropbox(oauth_result.access_token)
"""

def __init__(self, consumer_key, consumer_secret, locale=None, token_access_type='legacy'): # noqa: E501; pylint: disable=useless-super-delegation
def __init__(self, consumer_key, consumer_secret, locale=None, token_access_type='legacy',
scope=None, include_granted_scopes=None):
# noqa: E501; pylint: disable=useless-super-delegation
"""
Construct an instance.

Expand All @@ -258,12 +291,21 @@ def __init__(self, consumer_key, consumer_secret, locale=None, token_access_type
legacy - creates one long-lived token with no expiration
online - create one short-lived token with an expiration
offline - create one short-lived token with an expiration with a refresh token
:param list scope: list of scopes to request in base oauth flow. If left blank,
will default to all scopes for app
:param str include_granted_scopes: which scopes to include from previous grants
From the following enum:
user - include user scopes in the grant
team - include team scopes in the grant
Note: if this user has never linked the app, include_granted_scopes must be None
"""
super(DropboxOAuth2FlowNoRedirect, self).__init__(
consumer_key,
consumer_secret,
locale,
token_access_type,
scope,
include_granted_scopes,
)

def start(self):
Expand All @@ -275,7 +317,8 @@ def start(self):
access the user's Dropbox account. Tell the user to visit this URL
and approve your app.
"""
return self._get_authorize_url(None, None, self.token_access_type)
return self._get_authorize_url(None, None, self.token_access_type, self.scope,
self.include_granted_scopes)

def finish(self, code):
"""
Expand Down Expand Up @@ -337,7 +380,8 @@ def dropbox_auth_finish(web_app_session, request):
"""

def __init__(self, consumer_key, consumer_secret, redirect_uri, session,
csrf_token_session_key, locale=None, token_access_type='legacy'):
csrf_token_session_key, locale=None, token_access_type='legacy',
scope=None, include_granted_scopes=None):
"""
Construct an instance.

Expand All @@ -361,9 +405,17 @@ def __init__(self, consumer_key, consumer_secret, redirect_uri, session,
legacy - creates one long-lived token with no expiration
online - create one short-lived token with an expiration
offline - create one short-lived token with an expiration with a refresh token
:param list scope: list of scopes to request in base oauth flow. If left blank,
will default to all scopes for app
:param str include_granted_scopes: which scopes to include from previous grants
From the following enum:
user - include user scopes in the grant
team - include team scopes in the grant
Note: if this user has never linked the app, include_granted_scopes must be None
"""
super(DropboxOAuth2Flow, self).__init__(consumer_key, consumer_secret,
locale, token_access_type)
super(DropboxOAuth2Flow, self).__init__(consumer_key, consumer_secret, locale,
token_access_type, scope,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest passing kwargs with keyword

include_granted_scopes)
self.redirect_uri = redirect_uri
self.session = session
self.csrf_token_session_key = csrf_token_session_key
Expand Down Expand Up @@ -397,7 +449,8 @@ def start(self, url_state=None):
state += "|" + url_state
self.session[self.csrf_token_session_key] = csrf_token

return self._get_authorize_url(self.redirect_uri, state, self.token_access_type)
return self._get_authorize_url(self.redirect_uri, state, self.token_access_type,
self.scope, self.include_granted_scopes)

def finish(self, query_params):
"""
Expand Down
25 changes: 25 additions & 0 deletions example/commandline-oauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env python3

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth adding document right now. Saying that this example is only for certain partners.

Otherwise this might confuse people. The example/readme part in Github is educational. So we want to make sure they are as clear as possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually will change this to a working example so people can use it today

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool


import dropbox
from dropbox import DropboxOAuth2FlowNoRedirect

APP_KEY = ""
APP_SECRET = ""

auth_flow = DropboxOAuth2FlowNoRedirect(APP_KEY, APP_SECRET)

authorize_url = auth_flow.start()
print("1. Go to: " + authorize_url)
print("2. Click \"Allow\" (you might have to log in first).")
print("3. Copy the authorization code.")
auth_code = input("Enter the authorization code here: ").strip()

try:
oauth_result = auth_flow.finish(auth_code)
print(oauth_result)
except Exception as e:
print('Error: %s' % (e,))

dbx = dropbox.Dropbox(oauth2_access_token=oauth_result.access_token,
app_key=APP_KEY, app_secret=APP_SECRET)
dbx.users_get_current_account()
3 changes: 2 additions & 1 deletion test/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pytest
mock
pytest-mock
pytest-mock
Loading