@@ -184,6 +184,8 @@ class CAS(object):
184
184
The path to the SSL certificates for the CAS server.
185
185
authcode : string, optional
186
186
Authorization code from SASLogon used to retrieve an OAuth token.
187
+ pkce : boolean, optional
188
+ Use Proof Key for Code Exchange to obtain the Authorization code
187
189
**kwargs : any, optional
188
190
Arbitrary keyword arguments used for internal purposes only.
189
191
@@ -353,7 +355,7 @@ def _get_connection_info(cls, hostname, port, username, password, protocol, path
353
355
def __init__ (self , hostname = None , port = None , username = None , password = None ,
354
356
session = None , locale = None , nworkers = None , name = None ,
355
357
authinfo = None , protocol = None , path = None , ssl_ca_list = None ,
356
- authcode = None , ** kwargs ):
358
+ authcode = None , pkce = False , ** kwargs ):
357
359
358
360
# Filter session options allowed as parameters
359
361
_kwargs = {}
@@ -399,11 +401,23 @@ def __init__(self, hostname=None, port=None, username=None, password=None,
399
401
soptions = a2n (getsoptions (session = session , locale = locale ,
400
402
nworkers = nworkers , protocol = protocol ))
401
403
404
+ # Check for Proof Key for Code Exchange
405
+ pkce = pkce or cf .get_option ('cas.pkce' )
402
406
# Check for authcode authentication
403
407
authcode = authcode or cf .get_option ('cas.authcode' )
404
- if protocol in ['http' , 'https' ] and authcode :
408
+ if protocol in ['http' , 'https' ] and ( authcode or pkce ) :
405
409
username = None
406
- password = type (self )._get_token (authcode = authcode , url = hostname )
410
+ verifystring = None
411
+ if pkce :
412
+ if authcode :
413
+ # User will be prompted for authcode,
414
+ # do not enter it in CAS() when using pkce
415
+ raise SWATError ('Do not specify authcode with pkce' )
416
+ # Get the authcode from SASLogon using Proof Key for Code Exchange
417
+ authcode , verifystring = type (self )._get_authcode (url = hostname )
418
+ # Get the OAuth token from SASLogon
419
+ password = type (self )._get_token (authcode = authcode , url = hostname ,
420
+ verifystring = verifystring , pkce = pkce )
407
421
408
422
# Create error handler
409
423
try :
@@ -538,7 +552,8 @@ def _id_generator():
538
552
539
553
@classmethod
540
554
def _get_token (cls , username = None , password = None , authcode = None ,
541
- client_id = None , client_secret = None , url = None ):
555
+ client_id = None , client_secret = None , url = None ,
556
+ verifystring = None , pkce = False ):
542
557
''' Retrieve token from Viya installation '''
543
558
from .rest .connection import _print_request , _setup_ssl
544
559
@@ -552,10 +567,21 @@ def _get_token(cls, username=None, password=None, authcode=None,
552
567
client_id = client_id or cf .get_option ('cas.client_id' ) or 'SWAT'
553
568
554
569
authcode = authcode or cf .get_option ('cas.authcode' )
570
+ pkce = pkce or cf .get_option ('cas.pkce' )
571
+
555
572
if authcode :
556
573
client_secret = client_secret or cf .get_option ('cas.client_secret' ) or ''
557
- body = {'grant_type' : 'authorization_code' , 'code' : authcode ,
558
- 'client_id' : client_id , 'client_secret' : client_secret }
574
+
575
+ if pkce :
576
+ if verifystring is None :
577
+ raise SWATError ('A code verifier must be supplied for pkce' )
578
+
579
+ body = {'grant_type' : 'authorization_code' ,
580
+ 'code' : authcode , 'code_verifier' : verifystring ,
581
+ 'client_id' : client_id , 'client_secret' : client_secret }
582
+ else :
583
+ body = {'grant_type' : 'authorization_code' , 'code' : authcode ,
584
+ 'client_id' : client_id , 'client_secret' : client_secret }
559
585
else :
560
586
username = username or cf .get_option ('cas.username' )
561
587
password = password or cf .get_option ('cas.token' )
@@ -567,11 +593,58 @@ def _get_token(cls, username=None, password=None, authcode=None,
567
593
data = urlencode (body ))
568
594
569
595
if resp .status_code >= 300 :
596
+ logger .debug ('Token request resulted in status code %d : \n %s' ,
597
+ resp .status_code , resp .json ())
570
598
raise SWATError ('Token request resulted in a status of %s' %
571
599
resp .status_code )
572
600
573
601
return resp .json ()['access_token' ]
574
602
603
+ @classmethod
604
+ def _get_authcode (cls , url = None , client_id = None , client_secret = None ):
605
+ '''
606
+ Generate the Proof Key for Code Exchange URL to retrieve the authentication code
607
+ from the Viya installation.
608
+ Wait for the user to provide the authentication code
609
+ '''
610
+ try :
611
+ # The secrets package was introduced in Python 3.6
612
+ import secrets
613
+ except ImportError :
614
+ raise SWATError ("Python 3.6 or later is required for "
615
+ "Proof Key for Code Exchange." )
616
+
617
+ import hashlib
618
+ import base64
619
+
620
+ client_id = client_id or cf .get_option ('cas.client_id' ) or 'SWAT'
621
+ client_secret = client_secret or cf .get_option ('cas.client_secret' ) or ''
622
+
623
+ # Generate the URL for the authcode request
624
+ cv = secrets .token_urlsafe (32 )
625
+ cvh = hashlib .sha256 (cv .encode ('ascii' )).digest ()
626
+ cvhe = base64 .urlsafe_b64encode (cvh )
627
+ cc = cvhe .decode ('ascii' )[:- 1 ]
628
+ # Note, for pkce "cc" is provided in the authcode request
629
+ # and "cv" is provided in the OAuth token request
630
+ purl = ("/SASLogon/oauth/authorize?client_id={}&response_type=code"
631
+ "&code_challenge_method=S256&code_challenge={}" ).format (client_id , cc )
632
+ authurl = urljoin (url , purl )
633
+
634
+ # Display the URL to the user and wait while they go off and get the authcode
635
+ # to respond to the prompt
636
+ msg = ("Please enter the authorization code obtained from the following url : "
637
+ "\n {} \n " ).format (authurl )
638
+ authcode = input (msg )
639
+
640
+ # trim leading trailing whitespace and verify something was entered
641
+ authcode = authcode .strip ()
642
+ if len (authcode ) == 0 :
643
+ raise SWATError (
644
+ "You must provide an authorization code to connect to the CAS server" )
645
+
646
+ return authcode , cv
647
+
575
648
def _gen_id (self ):
576
649
''' Generate an ID unique to the session '''
577
650
import numpy
0 commit comments