1313import base64
1414import sys
1515import functools
16+ import random
17+ import string
1618
1719import requests
1820
@@ -129,6 +131,10 @@ def __init__(
129131 This does not apply if you have chosen to pass your own Http client.
130132
131133 """
134+ if not server_configuration :
135+ raise ValueError ("Missing input parameter server_configuration" )
136+ if not client_id :
137+ raise ValueError ("Missing input parameter client_id" )
132138 self .configuration = server_configuration
133139 self .client_id = client_id
134140 self .client_secret = client_secret
@@ -353,38 +359,142 @@ def obtain_token_by_device_flow(self,
353359 return result
354360 time .sleep (1 ) # Shorten each round, to make exit more responsive
355361
362+ def _build_auth_request_uri (
363+ self ,
364+ response_type , redirect_uri = None , scope = None , state = None , ** kwargs ):
365+ if "authorization_endpoint" not in self .configuration :
366+ raise ValueError ("authorization_endpoint not found in configuration" )
367+ authorization_endpoint = self .configuration ["authorization_endpoint" ]
368+ params = self ._build_auth_request_params (
369+ response_type , redirect_uri = redirect_uri , scope = scope , state = state ,
370+ ** kwargs )
371+ sep = '&' if '?' in authorization_endpoint else '?'
372+ return "%s%s%s" % (authorization_endpoint , sep , urlencode (params ))
373+
356374 def build_auth_request_uri (
357375 self ,
358376 response_type , redirect_uri = None , scope = None , state = None , ** kwargs ):
377+ # This method could be named build_authorization_request_uri() instead,
378+ # but then there would be a build_authentication_request_uri() in the OIDC
379+ # subclass doing almost the same thing. So we use a loose term "auth" here.
359380 """Generate an authorization uri to be visited by resource owner.
360381
382+ Parameters are the same as another method :func:`initiate_auth_code_flow()`,
383+ whose functionality is a superset of this method.
384+
385+ :return: The auth uri as a string.
386+ """
387+ warnings .warn ("Use initiate_auth_code_flow() instead. " , DeprecationWarning )
388+ return self ._build_auth_request_uri (
389+ response_type , redirect_uri = redirect_uri , scope = scope , state = state ,
390+ ** kwargs )
391+
392+ def initiate_auth_code_flow (
393+ # The name is influenced by OIDC
394+ # https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth
395+ self ,
396+ scope = None , redirect_uri = None , state = None ,
397+ ** kwargs ):
398+ """Initiate an auth code flow.
399+
361400 Later when the response reaches your redirect_uri,
362- you can use parse_auth_response() to check the returned state.
363-
364- This method could be named build_authorization_request_uri() instead,
365- but then there would be a build_authentication_request_uri() in the OIDC
366- subclass doing almost the same thing. So we use a loose term "auth" here.
367-
368- :param response_type:
369- Must be "code" when you are using Authorization Code Grant,
370- "token" when you are using Implicit Grant, or other
371- (possibly space-delimited) strings as registered extension value.
372- See https://tools.ietf.org/html/rfc6749#section-3.1.1
373- :param redirect_uri: Optional. Server will use the pre-registered one.
374- :param scope: It is a space-delimited, case-sensitive string.
401+ you can use :func:`~obtain_token_by_auth_code_flow()`
402+ to complete the authentication/authorization.
403+
404+ :param list scope:
405+ It is a list of case-sensitive strings.
375406 Some ID provider can accept empty string to represent default scope.
376- :param state: Recommended. An opaque value used by the client to
407+ :param str redirect_uri:
408+ Optional. If not specified, server will use the pre-registered one.
409+ :param str state:
410+ An opaque value used by the client to
377411 maintain state between the request and callback.
412+ If absent, this library will automatically generate one internally.
378413 :param kwargs: Other parameters, typically defined in OpenID Connect.
414+
415+ :return:
416+ The auth code flow. It is a dict in this form::
417+
418+ {
419+ "auth_uri": "https://...", // Guide user to visit this
420+ "state": "...", // You may choose to verify it by yourself,
421+ // or just let obtain_token_by_auth_code_flow()
422+ // do that for you.
423+ "...": "...", // Everything else are reserved and internal
424+ }
425+
426+ The caller is expected to::
427+
428+ 1. somehow store this content, typically inside the current session,
429+ 2. guide the end user (i.e. resource owner) to visit that auth_uri,
430+ 3. and then relay this dict and subsequent auth response to
431+ :func:`~obtain_token_by_auth_code_flow()`.
379432 """
380- if "authorization_endpoint" not in self .configuration :
381- raise ValueError ("authorization_endpoint not found in configuration" )
382- authorization_endpoint = self .configuration ["authorization_endpoint" ]
383- params = self ._build_auth_request_params (
384- response_type , redirect_uri = redirect_uri , scope = scope , state = state ,
433+ response_type = kwargs .pop ("response_type" , "code" ) # Auth Code flow
434+ # Must be "code" when you are using Authorization Code Grant.
435+ # The "token" for Implicit Grant is not applicable thus not allowed.
436+ # It could theoretically be other
437+ # (possibly space-delimited) strings as registered extension value.
438+ # See https://tools.ietf.org/html/rfc6749#section-3.1.1
439+ if "token" in response_type :
440+ # Implicit grant would cause auth response coming back in #fragment,
441+ # but fragment won't reach a web service.
442+ raise ValueError ('response_type="token ..." is not allowed' )
443+ flow = { # These data are required by obtain_token_by_auth_code_flow()
444+ "state" : state or "" .join (random .sample (string .ascii_letters , 16 )),
445+ "redirect_uri" : redirect_uri ,
446+ "scope" : scope ,
447+ }
448+ auth_uri = self ._build_auth_request_uri (
449+ response_type , ** dict (flow , ** kwargs ))
450+ flow ["auth_uri" ] = auth_uri
451+ return flow
452+
453+ def obtain_token_by_auth_code_flow (
454+ self ,
455+ auth_code_flow ,
456+ auth_response ,
457+ scope = None ,
458+ ** kwargs ):
459+ """With the auth_response being redirected back,
460+ validate it against auth_code_flow, and then obtain tokens.
461+
462+ :param dict auth_code_flow:
463+ The same dict returned by :func:`~initiate_auth_code_flow()`.
464+ :param dict auth_response:
465+ A dict based on query string received from auth server.
466+ :param scope:
467+ You don't usually need to use scope parameter here.
468+ Some Identity Provider allows you to provide
469+ a subset of what you specified during :func:`~initiate_auth_code_flow`.
470+
471+ :return:
472+ * A dict containing "access_token" and/or "id_token", among others,
473+ depends on what scope was used.
474+ (See https://tools.ietf.org/html/rfc6749#section-5.1)
475+ * A dict containing "error", optionally "error_description", "error_uri".
476+ (It is either `this <https://tools.ietf.org/html/rfc6749#section-4.1.2.1>`_
477+ or `that <https://tools.ietf.org/html/rfc6749#section-5.2>`_
478+ """
479+ if auth_code_flow .get ("state" ) != auth_response .get ("state" ):
480+ raise ValueError ("state mismatch: {} vs {}" .format (
481+ auth_code_flow .get ("state" ), auth_response .get ("state" )))
482+ if auth_response .get ("error" ): # It means the first leg encountered error
483+ return auth_response
484+ if scope and set (scope ) - set (auth_code_flow .get ("scope" , [])):
485+ raise ValueError (
486+ "scope must be None or a subset of %s" % auth_code_flow .get ("scope" ))
487+ assert auth_response .get ("code" ), "First leg's response should have code"
488+ return self ._obtain_token_by_authorization_code (
489+ auth_response ["code" ],
490+ redirect_uri = auth_code_flow .get ("redirect_uri" ),
491+ # Required, if "redirect_uri" parameter was included in the
492+ # authorization request, and their values MUST be identical.
493+ scope = scope or auth_code_flow .get ("scope" ),
494+ # It is both unnecessary and harmless, per RFC 6749.
495+ # We use the same scope already used in auth request uri,
496+ # thus token cache can know what scope the tokens are for.
385497 ** kwargs )
386- sep = '&' if '?' in authorization_endpoint else '?'
387- return "%s%s%s" % (authorization_endpoint , sep , urlencode (params ))
388498
389499 @staticmethod
390500 def parse_auth_response (params , state = None ):
@@ -394,6 +504,8 @@ def parse_auth_response(params, state=None):
394504 :param state: REQUIRED if the state parameter was present in the client
395505 authorization request. This function will compare it with response.
396506 """
507+ warnings .warn (
508+ "Use obtain_token_by_auth_code_flow() instead" , DeprecationWarning )
397509 if not isinstance (params , dict ):
398510 params = parse_qs (params )
399511 if params .get ('state' ) != state :
@@ -408,6 +520,9 @@ def obtain_token_by_authorization_code(
408520 but it can also be used by a device-side native app (Public Client).
409521 See more detail at https://tools.ietf.org/html/rfc6749#section-4.1.3
410522
523+ You are encouraged to use its higher level method
524+ :func:`~obtain_token_by_auth_code_flow` instead.
525+
411526 :param code: The authorization code received from authorization server.
412527 :param redirect_uri:
413528 Required, if the "redirect_uri" parameter was included in the
@@ -417,6 +532,13 @@ def obtain_token_by_authorization_code(
417532 We suggest to use the same scope already used in auth request uri,
418533 so that this library can link the obtained tokens with their scope.
419534 """
535+ warnings .warn (
536+ "Use obtain_token_by_auth_code_flow() instead" , DeprecationWarning )
537+ return self ._obtain_token_by_authorization_code (
538+ code , redirect_uri = redirect_uri , scope = scope , ** kwargs )
539+
540+ def _obtain_token_by_authorization_code (
541+ self , code , redirect_uri = None , scope = None , ** kwargs ):
420542 data = kwargs .pop ("data" , {})
421543 data .update (code = code , redirect_uri = redirect_uri )
422544 if scope :
0 commit comments