1010import warnings
1111import time
1212import base64
13+ import sys
1314
1415import requests
1516
1617
18+ string_types = (str ,) if sys .version_info [0 ] >= 3 else (basestring , )
19+
1720
1821class BaseClient (object ):
1922 # This low-level interface works. Yet you'll find its sub-class
@@ -163,13 +166,15 @@ def _obtain_token( # The verb "obtain" is influenced by OAUTH2 RFC 6749
163166 raise
164167
165168 def obtain_token_by_refresh_token (self , refresh_token , scope = None , ** kwargs ):
169+ # type: (str, Union[str, list, set, tuple]) -> dict
166170 """Obtain an access token via a refresh token.
167171
168172 :param refresh_token: The refresh token issued to the client
169173 :param scope: If omitted, is treated as equal to the scope originally
170174 granted by the resource ownser,
171175 according to https://tools.ietf.org/html/rfc6749#section-6
172176 """
177+ assert isinstance (refresh_token , string_types )
173178 data = kwargs .pop ('data' , {})
174179 data .update (refresh_token = refresh_token , scope = scope )
175180 return self ._obtain_token ("refresh_token" , data = data , ** kwargs )
@@ -328,7 +333,7 @@ def parse_auth_response(params, state=None):
328333 return params
329334
330335 def obtain_token_by_authorization_code (
331- self , code , redirect_uri = None , ** kwargs ):
336+ self , code , redirect_uri = None , scope = None , ** kwargs ):
332337 """Get a token via auhtorization code. a.k.a. Authorization Code Grant.
333338
334339 This is typically used by a server-side app (Confidential Client),
@@ -339,9 +344,15 @@ def obtain_token_by_authorization_code(
339344 :param redirect_uri:
340345 Required, if the "redirect_uri" parameter was included in the
341346 authorization request, and their values MUST be identical.
347+ :param scope:
348+ It is both unnecessary and harmless to use scope here, per RFC 6749.
349+ We suggest to use the same scope already used in auth request uri,
350+ so that this library can link the obtained tokens with their scope.
342351 """
343352 data = kwargs .pop ("data" , {})
344353 data .update (code = code , redirect_uri = redirect_uri )
354+ if scope :
355+ data ["scope" ] = scope
345356 if not self .client_secret :
346357 # client_id is required, if the client is not authenticating itself.
347358 # See https://tools.ietf.org/html/rfc6749#section-4.1.3
@@ -380,14 +391,10 @@ def __init__(self,
380391 self .on_removing_rt = on_removing_rt
381392 self .on_updating_rt = on_updating_rt
382393
383- def _obtain_token (self , grant_type , params = None , data = None ,
384- rt_getter = lambda token_item : token_item ["refresh_token" ],
385- * args , ** kwargs ):
394+ def _obtain_token (self , grant_type , params = None , data = None , * args , ** kwargs ):
386395 RT = "refresh_token"
387396 _data = data .copy () # to prevent side effect
388397 refresh_token = _data .get (RT )
389- if grant_type == RT and isinstance (refresh_token , dict ):
390- _data [RT ] = rt_getter (refresh_token ) # Put raw RT in _data
391398 resp = super (Client , self )._obtain_token (
392399 grant_type , params , _data , * args , ** kwargs )
393400 if "error" not in resp :
@@ -399,7 +406,9 @@ def _obtain_token(self, grant_type, params=None, data=None,
399406 scope = _resp ["scope" ].split () # It is conceptually a set,
400407 # but we represent it as a list which can be persisted to JSON
401408 else :
402- # TODO: Deal with absent scope in authorization grant
409+ # Note: The scope will generally be absent in authorization grant,
410+ # but our obtain_token_by_authorization_code(...) encourages
411+ # app developer to still explicitly provide a scope here.
403412 scope = _data .get ("scope" )
404413 self .on_obtaining_tokens ({
405414 "client_id" : self .client_id ,
@@ -416,31 +425,31 @@ def obtain_token_by_refresh_token(self, token_item, scope=None,
416425 on_removing_rt = None ,
417426 ** kwargs ):
418427 # type: (Union[str, dict], Union[str, list, set, tuple], Callable) -> dict
419- """This is an "overload" which accepts a refresh token item as a dict,
420- therefore this method can relay refresh_token item to event listeners.
428+ """This is an overload which will trigger token storage callbacks.
421429
422430 :param token_item:
423- A refresh token item as a dict, came from the cache managed by this lib.
431+ A refresh token (RT) item, in flexible format. It can be a string,
432+ or a whatever data structure containing RT string and its metadata,
433+ in such case the `rt_getter` callable must be able to
434+ extract the RT string out from the token item data structure.
435+
436+ Either way, this token_item will be passed into other callbacks as-is.
424437
425- Alternatively, you can still use a refresh token (RT) as a string,
426- supposedly came from a token cache managed by a different library,
427- then this library will store the new RT (if Authority Server issued one)
428- into this lib's cache. This is a way to migrate from other lib to us.
429438 :param scope: If omitted, is treated as equal to the scope originally
430439 granted by the resource ownser,
431440 according to https://tools.ietf.org/html/rfc6749#section-6
432- :param rt_getter: A callable used to extract the RT from token_item
441+ :param rt_getter: A callable to translate the token_item to a raw RT string
433442 :param on_removing_rt: If absent, fall back to the one defined in initialization
434443 """
435444 resp = super (Client , self ).obtain_token_by_refresh_token (
436- token_item , scope = scope ,
437- rt_getter = rt_getter , # Wire up this for _obtain_token()
445+ rt_getter (token_item )
446+ if not isinstance (token_item , string_types ) else token_item ,
447+ scope = scope ,
438448 ** kwargs )
439- if isinstance (token_item , dict ):
440- if resp .get ('error' ) == 'invalid_grant' :
441- (on_removing_rt or self .on_removing_rt )(token_item ) # Discard old RT
442- if 'refresh_token' in resp :
443- self .on_updating_rt (token_item , resp ['refresh_token' ])
449+ if resp .get ('error' ) == 'invalid_grant' :
450+ (on_removing_rt or self .on_removing_rt )(token_item ) # Discard old RT
451+ if 'refresh_token' in resp :
452+ self .on_updating_rt (token_item , resp ['refresh_token' ])
444453 return resp
445454
446455 def obtain_token_by_assertion (
0 commit comments