@@ -184,6 +184,8 @@ class CAS(object):
184184 The path to the SSL certificates for the CAS server.
185185 authcode : string, optional
186186 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
187189 **kwargs : any, optional
188190 Arbitrary keyword arguments used for internal purposes only.
189191
@@ -353,7 +355,7 @@ def _get_connection_info(cls, hostname, port, username, password, protocol, path
353355 def __init__ (self , hostname = None , port = None , username = None , password = None ,
354356 session = None , locale = None , nworkers = None , name = None ,
355357 authinfo = None , protocol = None , path = None , ssl_ca_list = None ,
356- authcode = None , ** kwargs ):
358+ authcode = None , pkce = False , ** kwargs ):
357359
358360 # Filter session options allowed as parameters
359361 _kwargs = {}
@@ -399,11 +401,23 @@ def __init__(self, hostname=None, port=None, username=None, password=None,
399401 soptions = a2n (getsoptions (session = session , locale = locale ,
400402 nworkers = nworkers , protocol = protocol ))
401403
404+ # Check for Proof Key for Code Exchange
405+ pkce = pkce or cf .get_option ('cas.pkce' )
402406 # Check for authcode authentication
403407 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 ) :
405409 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 )
407421
408422 # Create error handler
409423 try :
@@ -538,7 +552,8 @@ def _id_generator():
538552
539553 @classmethod
540554 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 ):
542557 ''' Retrieve token from Viya installation '''
543558 from .rest .connection import _print_request , _setup_ssl
544559
@@ -552,10 +567,21 @@ def _get_token(cls, username=None, password=None, authcode=None,
552567 client_id = client_id or cf .get_option ('cas.client_id' ) or 'SWAT'
553568
554569 authcode = authcode or cf .get_option ('cas.authcode' )
570+ pkce = pkce or cf .get_option ('cas.pkce' )
571+
555572 if authcode :
556573 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 }
559585 else :
560586 username = username or cf .get_option ('cas.username' )
561587 password = password or cf .get_option ('cas.token' )
@@ -567,11 +593,58 @@ def _get_token(cls, username=None, password=None, authcode=None,
567593 data = urlencode (body ))
568594
569595 if resp .status_code >= 300 :
596+ logger .debug ('Token request resulted in status code %d : \n %s' ,
597+ resp .status_code , resp .json ())
570598 raise SWATError ('Token request resulted in a status of %s' %
571599 resp .status_code )
572600
573601 return resp .json ()['access_token' ]
574602
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+
575648 def _gen_id (self ):
576649 ''' Generate an ID unique to the session '''
577650 import numpy
0 commit comments