With the shift towards the cloud, Federated Identity Management and authentication standards like SAML and OpenID Connect are becoming increasingly more common. Using these standards, validation of a user’s credentials is performed by a trusted identity provider and are no longer available to an application. SAS Viya supports these authentication standards natively through the SAS Logon Manager (SASLogon) microservice, and Viya web clients can make secondary connections to CAS by leveraging their OAuth token received from SASLogon during initial authentication.
However, when using open source programming clients, in conjunction with Viya SWAT libraries, not having a user’s credentials (password or Kerberos ticket) can make connections to CAS troublesome, as these clients do not directly interact with the SASLogon microservice.
This blog post explores a solution to provide a Python/R SWAT interface to CAS using a Jupyter Notebook programming environment launched directly from SAS Drive, without the need for direct user credentials.
Jupyter Notebook is an open-source web-based interactive programming environment allowing a user to create and share documents containing live code, equations, and visualizations. Jupyter Notebooks are a great way to leverage the power of SAS Viya/CAS using the Python or R SWAT packages. A SAS Kernel for Jupyter Notebooks also exists allowing a user to write, document, and submit SAS programming statements to SAS 9 workspaces or Viya compute sessions.
The JupyterHub project provides a way to scale a notebook to multiple users through a Hub that spawns, manages, and proxies multiple instances of a single-user Jupyter Notebook.
At a high level JupyterHub consists of the following pieces:
In order to provide access to CAS from Python/R SWAT without explicit user credentials, we can take advantage of the extensibility of the JupyterHub by using an OAuth token to authenticate to CAS. SASLogon is an OAuth 2.0 Identity Provider and Jupyter Hub extends to use a custom OAuth authenticator. The OAuth Access/Refresh tokens generated by SASLogon during authentication can be made available from the Authenticator to the end-user notebook session as environment variables. In turn, these tokens are used with the OAuth authentication mechanism available to the binary connection to CAS in the R/Python SWAT packages.
At a high level the steps to implement this solution follow:
Note: This blog post will not cover the steps required to set up a JupyterHub server from scratch. We will assume an existing hub already exists and is configured with the default Authenticator.
Use the SAS Viya Consul token to obtain a SASLogon access token in order to register a new application:
As a sudo user, run the following commands from the server where consul lives. Update VIYA_BASE_URL with the base url used to access Viya web applications.
export CONSUL_TOKEN=`sudo cat /opt/sas/viya/config/etc/SASSecurityCertificateFramework/tokens/consul/default/client.token`
export VIYA_BASE_URL=https://viya.demo.sas.com
curl -k -X POST "$VIYA_BASE_URL/SASLogon/oauth/clients/consul?callback=false&serviceId=app" \
-H "X-Consul-Token: $CONSUL_TOKEN"
The cURL command returns json similar to the following.
{"access_token":"eyJhbGciOiJSUzI1NiIsIm...","token_type":"bearer","expires_in":35999,"scope":"uaa.admin","jti":"de81c7f3cca645ac807f18dc0d186331"}
To assist in later use, create an environment variable from the access_token key returned in the JSON.
export ACCESS_TOKEN=”eyJhbGciOiJSUzI1NiIsIm...”
Register a new client using the access token with the authorization_code and refresh_token grant types. Update ‘export JUYPYTERHUB_BASE_URL=’ with the base URL to access JupyterHub, and client_id and client_secret with your own values.
export JUPYTERHUB_BASE_URL=https://jupyterhub.demo.sas.com
curl -k -X POST "$VIYA_BASE_URL/SASLogon/oauth/clients" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d "{
\"client_id\": \"your_client_id\",
\"client_secret\": \"your_client_secret\",
\"scope\": [\"openid\"],
\"authorized_grant_types\": [\"authorization_code\",\"refresh_token\"],
\"redirect_uri\": \"$JUPYTERHUB_BASE_URL/hub/oauth_callback\",
\"access_token_validity\": 1296000,
\"autoapprove\": true
}"
Installation instructions for the SWAT packages are available on the packages corresponding GitHub pages:
To make packages available to JupyterHub users, you generally install packages system-wide or in a shared environment, depending on how your Hub is configured. See the JupyterHub documentation for details specific to your configuration: https://jupyterhub.readthedocs.io/
As an example, if you installed JupyterHub at /opt/jupyterhub/ as root, installing the python SWAT package, would be as simple as the following command:
sudo /opt/jupyterhub/bin/pip3 install swat
Install the JupyterHub OAuthenticator package.
sudo /opt/jupyterhub/bin/pip3 install oauthenticator
The package is available from GitHub at https://github.com/jupyterhub/oauthenticator, and is installed with pip.The full documentation is available at https://oauthenticator.readthedocs.io/en/latest/. We will leverage the “Generic Authenticator” in order to integrate with SASLogon.
Next, add the code located on this sascommunities GitHub page to jupyterhub_config.py. Place the code under the comment section starting with “## Class for authenticating users”. Pay attention to preserve indentation. The code configures the Generic OAthenticator class to use SASLogon as its identity provider.
This step also enables authentication state (auth state) which allows the Authenticator to persist state information related to authentication in the internal JupyterHub database. Because auth state can contain sensitive information, it is encrypted before being stored, so an encryption key is set (JupyterHub uses the Fernet python package for encryption).
The pre_spawn_start method is a hook called before spawning a user’s notebook to pass state information. This method retrieves the OAuth access token from the auth state and passes it to the spawner environment as an environment variable.
The refresh_user method to refreshes the users OAuth tokens and updates the auth state if the token expires. The method executes before spawning a notebook since (refresh_pre_spawn = True), and every 6 hours (auth_refresh_age = 21600); however, the tokens from the auth state are only available to the notebook server once, through the pre_spawn_start method, when the spawner launches the server. Therefore, a token may appear expired if notebook servers run longer than the access token validity period.
When registering the JupyterHub client via curl, the access token validity was set to 1296000 seconds. Implement a cull_idle_servers script to make sure no notebook servers left behind by users that are older than 15 days (1296000 seconds).
Make sure to replace the references of viya.demo.sas.com with the FQDN used to access SAS Viya and jupyterhub.demo.sas.com with the FQDN for JupyterHub. Also, update the client_id and client_secret with the values you registered earlier. Otherwise the code can remain unchanged.
If you receive a CERTIFICATE_VERIFIED_FAILED error, it most likely means that the CA certificate used to sign the Viya HTTP server is not available to Python. You can either uncomment the “c.GenericOAuthenticator.tls_verify = False” line in the code, or add the CA certificate to the JupyterHub server’s truststore.
If you saved the link to a location in SAS Content, available to other users, they can navigate to the location in SAS Content from the “All” tab in SAS Drive. From there they can pin it to their quick access if you do not wish to administrate all users quick access links.
Authentication to JupyterHub is automatic after clicking the Quick Access link from SAS Drive.
You may also log into JupyterHub directly, without going through SAS Drive. In that case, if you are not already authenticated to SAS Viya, you will be redirected to SASLogon (and then to any other downstream identity providers if applicable). Once authenticated to SAS Viya you are redirected back to JupyterHub
To connect to CAS, supply the OAuth token environment variable from the pre_spawn_start method to the password field in the binary protocol connection constructor.
import os, swat
conn = swat.CAS('cas.demo.sas.com', 5570, password=os.environ.get('ACCESS_TOKEN'))
library(swat)
conn <- swat::CAS('cas.demo.sas.com', port=5570, password=Sys.getenv('ACCESS_TOKEN'))
If you have the CASCLIENTDEBUG environment variable set, you should see similar messages in the log:
NOTE: Client is using the oauth identity provider
NOTE: Sent challenge length 1212
NOTE: Received response length 55
NOTE: User viyademo connected to CAS using OAuth 2.
In cloud-based environments with federated authentication mechanisms, users may not have credentials available to authenticate to the CAS server using SWAT clients. Leveraging OAuth tokens generated by SAS Logon, may be the only option available. The Jupyter authenticator insulates the programmer from being concerned with how to generate their own OAuth token.
In addition, integrating JupyterHub with SASLogon and SAS Drive is a great way to provide a portal to programming environment for users who wish to leverage the power of the CAS analytic engine, in a more familiar programming language, from the same area where all of their SAS Content and Reports are available and organized.
Are you ready for the spotlight? We're accepting content ideas for SAS Innovate 2025 to be held May 6-9 in Orlando, FL. The call is open until September 25. Read more here about why you should contribute and what is in it for you!
Data Literacy is for all, even absolute beginners. Jump on board with this free e-learning and boost your career prospects.