Skip to content

Commit 9ebaeb4

Browse files
committed
add support for PKCE in HTTP access method
1 parent 715cc3d commit 9ebaeb4

File tree

4 files changed

+67
-12
lines changed

4 files changed

+67
-12
lines changed

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,31 @@
22

33

44

5+
## [5.12.0] - 2024-04-18
6+
7+
### Added
8+
9+
- `None` Nothing added
10+
11+
### Changed
12+
13+
- `Enhancement` A new requirement in Viya 4 called for a change in the SSO authentication mechanism, to support PKCE. So this
14+
release provides support for that. There's nothing you have to do, but you will see a different message about the URL you need
15+
to use to get an auth code to provide, when using that authentication mechanism. In short, every time you want to authenticate,
16+
you get a new URL (it has its own unique code in it). This is displayed in the log same as the previous URL was, but unlike
17+
previously, where the url was the same every time for the deployment, and you could already get a code and provide it to SASsession(),
18+
this is a unique URL every time. So you can't get an authcode ahead of time. Don't fuss at me, I don't like it either. If you need
19+
help, open an issue and I'll see what I can do.
20+
21+
### Fixed
22+
23+
- `None` Nothing fixed
24+
25+
### Removed
26+
27+
- `None` Nothing removed
28+
29+
530
## [5.11.0] - 2024-04-16
631

732
### Added

saspy/sasiohttp.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
import ssl
2626
import atexit
2727

28+
import secrets
29+
import hashlib
30+
import base64
31+
2832
import tempfile as tf
2933
from time import sleep
3034
from threading import Thread
@@ -83,6 +87,7 @@ def __init__(self, session, **kwargs):
8387
puser = cfg.get('proxy_user', '')
8488
ppw = cfg.get('proxy_pw', '')
8589
pauthkey = cfg.get('proxy_authkey', '')
90+
self.pkce = cfg.get('pkce', None)
8691

8792
try:
8893
self.outopts = getattr(SAScfg, "SAS_output_options")
@@ -271,6 +276,15 @@ def __init__(self, session, **kwargs):
271276
else:
272277
self.verify = bool(inver)
273278

279+
inpkce = kwargs.get('pkce', None)
280+
if inpkce is not None:
281+
if lock and self.pkce:
282+
logger.warning("Parameter 'pkce' passed to SAS_session was ignored due to configuration restriction.")
283+
else:
284+
self.pkce = inpkce
285+
if self.pkce is None:
286+
self.pkce = True
287+
274288
if len(self.url) > 0:
275289
http = self.url.split('://')
276290
hp = http[1].split(':')
@@ -297,6 +311,7 @@ def __init__(self, session, **kwargs):
297311
else:
298312
self.port = 80
299313

314+
cv = None
300315
if not self._token and not authcode and not jwt and not self.serverid:
301316
found = False
302317
if self.authkey:
@@ -352,12 +367,23 @@ def __init__(self, session, **kwargs):
352367
raise RuntimeError("Neither authcode nor userid provided.")
353368

354369
if code_pw.lower() == 'authcode':
355-
purl = "/SASLogon/oauth/authorize?client_id={}&response_type=code".format(client_id)
370+
if self.pkce:
371+
cv = secrets.token_urlsafe(32)
372+
cvh = hashlib.sha256(cv.encode('ascii')).digest()
373+
cvhe = base64.urlsafe_b64encode(cvh)
374+
cc = cvhe.decode('ascii')[:-1]
375+
purl = "/SASLogon/oauth/authorize?client_id={}&response_type=code&code_challenge_method=S256&code_challenge={}".format(client_id, cc)
376+
else:
377+
purl = "/SASLogon/oauth/authorize?client_id={}&response_type=code".format(client_id)
378+
356379
if len(self.url) > 0:
357380
purl = self.url+purl
358381
else:
359382
purl = "http{}://{}:{}{}".format('s' if self.ssl else '', self.ip, str(self.port), purl)
360-
msg = "The default url to authenticate with would be {}\n".format(purl)
383+
if self.pkce:
384+
msg = "The PKCE required url to authenticate with is {}\n".format(purl)
385+
else:
386+
msg = "The default url to authenticate with would be {}\n".format(purl)
361387
msg += "Please enter authcode: "
362388
authcode = self._prompt(msg)
363389
if authcode is None:
@@ -472,7 +498,7 @@ def __init__(self, session, **kwargs):
472498

473499
# get AuthToken
474500
if not self._token:
475-
js = self._authenticate(user, pw, authcode, client_id, client_secret, jwt)
501+
js = self._authenticate(user, pw, authcode, client_id, client_secret, jwt, cv)
476502
self._token = js.get('access_token', None)
477503
self._refresh = js.get('refresh_token', None)
478504

@@ -557,7 +583,7 @@ def __init__(self, session, **kwargs):
557583

558584
return
559585

560-
def _authenticate(self, user, pw, authcode, client_id, client_secret, jwt):
586+
def _authenticate(self, user, pw, authcode, client_id, client_secret, jwt, cv):
561587

562588
if self.serverid:
563589
return {'access_token':'tom'}
@@ -566,9 +592,13 @@ def _authenticate(self, user, pw, authcode, client_id, client_secret, jwt):
566592
uauthcode = urllib.parse.quote(authcode)
567593
uclient_id = urllib.parse.quote(client_id)
568594
uclient_secret = urllib.parse.quote(client_secret)
569-
d1 = ("grant_type=authorization_code&code="+uauthcode+
570-
"&client_id="+uclient_id+"&client_secret="+uclient_secret).encode(self.encoding)
571595
headers = {"Accept":"application/vnd.sas.compute.session+json","Content-Type":"application/x-www-form-urlencoded"}
596+
if self.pkce:
597+
d1 = ("grant_type=authorization_code&code="+uauthcode+"&code_verifier="+cv+
598+
"&client_id="+uclient_id+"&client_secret="+uclient_secret).encode(self.encoding)
599+
else:
600+
d1 = ("grant_type=authorization_code&code="+uauthcode+
601+
"&client_id="+uclient_id+"&client_secret="+uclient_secret).encode(self.encoding)
572602
elif jwt:
573603
ujwt = urllib.parse.quote(jwt)
574604
d1 = "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion="+ujwt

saspy/sasiostdio.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -557,11 +557,6 @@ def _endsas(self):
557557
self.stdin.flush()
558558
sleep(1)
559559

560-
try:
561-
self._log += self.stderr.get_nowait().decode(self.sascfg.encoding, errors='replace').replace(chr(12), chr(10))
562-
except Empty:
563-
pass
564-
565560
if self.pid:
566561
if os.name == 'nt':
567562
self.pid.stdin.close()
@@ -591,6 +586,11 @@ def _endsas(self):
591586
self.to.join(1)
592587
self.te.join(1)
593588

589+
try:
590+
self._log += self.stderr.get_nowait().decode(self.sascfg.encoding, errors='replace').replace(chr(12), chr(10))
591+
except Empty:
592+
pass
593+
594594
if self.sascfg.verbose:
595595
logger.info("SAS Connection terminated. Subprocess id was "+str(pid))
596596
self.pid = None

saspy/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '5.11.0'
1+
__version__ = '5.12.0'

0 commit comments

Comments
 (0)