13
13
import base64
14
14
import sys
15
15
import functools
16
+ import random
17
+ import string
16
18
17
19
import requests
18
20
@@ -129,6 +131,10 @@ def __init__(
129
131
This does not apply if you have chosen to pass your own Http client.
130
132
131
133
"""
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" )
132
138
self .configuration = server_configuration
133
139
self .client_id = client_id
134
140
self .client_secret = client_secret
@@ -353,38 +359,142 @@ def obtain_token_by_device_flow(self,
353
359
return result
354
360
time .sleep (1 ) # Shorten each round, to make exit more responsive
355
361
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
+
356
374
def build_auth_request_uri (
357
375
self ,
358
376
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.
359
380
"""Generate an authorization uri to be visited by resource owner.
360
381
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
+
361
400
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.
375
406
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
377
411
maintain state between the request and callback.
412
+ If absent, this library will automatically generate one internally.
378
413
: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()`.
379
432
"""
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.
385
497
** kwargs )
386
- sep = '&' if '?' in authorization_endpoint else '?'
387
- return "%s%s%s" % (authorization_endpoint , sep , urlencode (params ))
388
498
389
499
@staticmethod
390
500
def parse_auth_response (params , state = None ):
@@ -394,6 +504,8 @@ def parse_auth_response(params, state=None):
394
504
:param state: REQUIRED if the state parameter was present in the client
395
505
authorization request. This function will compare it with response.
396
506
"""
507
+ warnings .warn (
508
+ "Use obtain_token_by_auth_code_flow() instead" , DeprecationWarning )
397
509
if not isinstance (params , dict ):
398
510
params = parse_qs (params )
399
511
if params .get ('state' ) != state :
@@ -408,6 +520,9 @@ def obtain_token_by_authorization_code(
408
520
but it can also be used by a device-side native app (Public Client).
409
521
See more detail at https://tools.ietf.org/html/rfc6749#section-4.1.3
410
522
523
+ You are encouraged to use its higher level method
524
+ :func:`~obtain_token_by_auth_code_flow` instead.
525
+
411
526
:param code: The authorization code received from authorization server.
412
527
:param redirect_uri:
413
528
Required, if the "redirect_uri" parameter was included in the
@@ -417,6 +532,13 @@ def obtain_token_by_authorization_code(
417
532
We suggest to use the same scope already used in auth request uri,
418
533
so that this library can link the obtained tokens with their scope.
419
534
"""
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 ):
420
542
data = kwargs .pop ("data" , {})
421
543
data .update (code = code , redirect_uri = redirect_uri )
422
544
if scope :
0 commit comments