Welcome back to my series on securely integrating custom applications into your SAS Viya platform. My goal today is to lay out some examples of the concepts I introduced in the previous posts. As a quick recap:
- In the first installment of this series I shared my experiences on a customer project, where we helped achieve value-add on top of their SAS Viya platform. We built a custom application for their buyers to use at auto auctions and embedded repeatable analytics from a SAS Viya decisioning workflow. That customization now helps their buyers make data-driven decisions based on both business rules and analytical models trained by historical data from past purchases and sales. The models are re-trained by the workflow as buyers "commit" new purchases.
- In part two, I outlined the security frameworks (OAuth 2.0 and OpenID Connect) and main integration points for bringing custom apps and SAS Viya together. I also introduced the main decision points before setting up this integration.
- The most recent post detailed the OAuth flow choices for access tokens from SAS Logon used for custom application and SAS Viya integration. I outlined some suggestions on when to use each flow in order to retrieve the appropriate scoping for your app.
I would encourage you to check out the previous parts in this series before reading on so we all start on a level playing field. In this post, I will assume you have, and thus are familiar with the concepts of user- and general-scoping. Before we jump into some examples for both of those scenarios, two things to note:
- Recall for both user-scoped and general-scoped scenarios, the Password authentication flow is technically valid, but the industry is suggesting phasing out the use of this flow (source one) (source two). Accordingly, I won't include examples of it here.
- This post assumes that you are familiar with the SAS Administration concepts of Custom Groups, granting access to SAS Content, and creating authorization rules. If those topics are unfamiliar to you, and you will be implementing this custom application registration, please take a look at the resources linked inline.
General-scoped access tokens
This refers to scenarios where we don't want end users to have to log in (directly to SAS Viya, at least). You don't want the tokens returned by SAS Logon to your custom applications generating authorization decisions based on the individual using your application. In these cases, however, you do still want control over the endpoints/resources your app calls and which CRUD operations (which map to HTTP methods) it performs on them. The general steps are:
- Create a Custom Group for the use of the application.
- Grant necessary access to that Custom Group for the appropriate endpoints and resources.
- Register a new, unique client to your SAS Viya environment, using the
client_credentials
grant type, and specifying the created Custom Group as the value for theauthorities
property.
Detailed steps
-
As a member of your SAS Viya environment's SAS Administrators group, create a new Custom Group for your application. The naming convention of App.<your_app> is recommended. Using prefixes in this manner logically organizes Custom Groups used for similar purposes together. Consider including a description like the one pictured below.
-
Determine the necessary resource(s), content, and/or endpoint(s) application users need. Next, create the authorization rules to provide appropriate access using the Custom Group you created in the previous step. I've often found this to be an iterative process which is perfectly fine. You (a SAS Admin) can adjust the authorization rules granted to this Custom Group, and thus your app, at any time.
_ -
Review the general documentation provided for registering clients to your SAS Viya environment. The following is an adaptation of those instructions, please take note of the differences and be sure to include them as you see below.
For readability, the required data for the last step above was:
{ "client_id": "{{YOUR_APP_NAME}}", "client_secret": "{{YOUR_SECRET}}", "authorized_grant_types": "client_credentials", "authorities": "{{CUSTOM_GROUP_FROM_PREVIOUS_STEPS}}", "scope": "openid" }
Where
scope
should include openid for authentication purposes.And where
authorities
will include your Custom Group linking this client with the authorization rules you created. This effectively grants those same permissions to any downstream API requests made by your application when it requests a token in the manner below (next step).
_ - Once registered, here is an example (using Postman) of how your application requests a token using the Client Credentials flow:
_
- And here is an example of how your application would make an API call using that token. In this example, an authorization rule was created for this endpoint with Read (GET) permissions for the Custom Group linked to this client:
Some important things to note
- You'll want to use dynamic variables instead of hard coded values as you see in the Postman calls above. This is just for illustrative purposes.
- If you make a GET request against the client you just registered for your application, you will not get back the
client_secret
value you specified upon creation. Note that as we saw in Step 4 above, your client will need this value to request it's tokens so plan accordingly. - In addition to any values that could change depending on where your application is deployed (including hostnames, etc.), ensure your
client_id
andclient_secret
values are secured properly. If you're using Docker, feed an environment variables file to docker-compose.yml. Make sure to include the variables file in your gitignore file. Moreover, if you're using k8s you can always use secrets. Just have a plan and never hard code these into your application. - When making requests to SAS Logon Manager for tokens within the context of your registered application as shown in the first screenshot above, the default timeout for the tokens returned is just shy of 12 hours. You can augment that by adding the
access_token_validity
property with a numeric value in seconds when you register your client. Any subsequent tokens obtained using your client will use that timeout value. - OAuth's Refresh Token flow isn't available in combination with the
client_credentials
grant type. Keep this in mind when deciding on the value to use for theaccess_token_validity
property. You may want to consider adding logic to your application to request a new token before the current one expires. Parsing out the value of theexpires_in
property dynamically from the original token request response will be helpful in accomplishing that. Keep in mind you will want enough of a time buffer if your application makes a string of endpoint requests or any long-running requests, so your process does not get interrupted by an expired token.
_
User-scoped access tokens
This refers to scenarios where it does make sense for your application's end users to log in to SAS. All subsequent API calls into SAS Viya resources your application makes should be within the context of what they, as an individual user, is allowed to do/see/access in SAS Viya. The authorization decisions for those requests will be based on the union of anything the logged-in user could do while logged into a native SAS Viya UI (see autoapprove
below) and any authorization rules that were explicitly granted to the Custom Group(s) selected while registering this client. A SAS Administrator can place all the users of your application in that Custom Group, ensuring a similar level of functionality for those users, while still obeying any more-granular rules that might apply, such as on specific data or content. The general steps are:
- Create a Custom Group for the use of the application
- Grant necessary access to that Custom Group for the appropriate endpoints and resources.
- Register a new, unique client to your SAS Viya environment, using the
authorization_code
grant type, specifying the created Custom Group as the value in thescope
property, and some additional specific properties shown below - Include logic in your application to handle the redirection to SAS Logon Manager when needed and to temporarily store authorization codes and OAuth tokens per session/user.
Detailed steps: prepping and registering the client
-
Perform steps 1 and 2 from the General-scoped tokens section above.
_ -
Review the general documentation provided for registering clients to your SAS Viya environment. The following is an adaptation of those instructions, please take note of the differences and be sure to include them as you see below.
For readability, the required data for the last step above was:
{ "client_id": "{{YOUR_APP_NAME}}", "client_secret": "{{YOUR_SECRET}}", "authorized_grant_types": ["authorization_code", "refresh_token"], "scope": ["openid", "{{CUSTOM_GROUP_FROM_PREVIOUS_STEPS"], "redirect_uri": ["{{URL_SASLOGON_SHOULD_REDIRECT_BACK_TO}}/callback"], "autoapprove": "{{TRUE_OR_FALSE_IN_LOWCASE}}" }
Where
scope
should include openid for authentication purposes and, for this authorization flow, must also include the Custom Group you created. This is what links your client to any authorization rules you created, effectively granting those same permissions to any downstream API requests made by your application.And where
redirect_uri
includes the URL SAS Logon Manager sends your users back to after they authenticate. It's formatted in a list above in case you want to include both HTTP and HTTPS protocols for development purposes. This is helpful in avoiding edits to this client down the road. Take note of the inclusion of the/callback
postfix in the screenshot above. We'll see more on this later.And where
autoapprove
is either true or false, depending on whether or not you want to present the user with a list of Custom Groups they need to approve or deny before proceeding. My personal opinion is to stick with true, not only because it more-closely mimics the behavior already present with native SAS Viya applications. In addition, the user likely won't know the implications of selecting (or not selecting) certain Custom Groups. This could lead to downstream authorization issues for your application.
_
Detailed steps: partial example for your app's logic
Your app has several requirements pertaining to authentication and authorization. First, it needs to know when to redirect users to SAS Logon Manager (when they're not already logged in or if their session expired). The app also must handle the authorization code returned to your user. Further, it needs to request an OAuth token for your user and how to use that token to make downstream requests to other APIs. Finally, your app needs to revoke tokens when necessary.
This is not a complete example and exactly how this is implemented depends heavily on the language your application was developed in. I don't want to distract from the topic at hand, so I'm not going to include complete code blocks. The application used for this example is a Javascript React web UI and I'll focus on the main order of operations involved in getting this to work. Therefore, I've intentionally removed some syntax to illustrate this in a more agnostic way.
- User navigates to our application's home page. Logic determines that a boolean state variable we're setting called isAuthenticated is false (default), so it redirects to the SAS Logon page using the URL below. Note that all items in {{ }} are substituted with environment variables rather than hardcoded. Recall, in the previous step when we registered the client, I mentioned you might want to include
/callback
after the URL to your application. When you're developing the routes in your code logic, it helps to differentiate between someone who has navigated directly to your/
or/home
page versus a user that's been redirected there from SAS Logon Manager. You'll see why in the next step.https://{{VIYA_HOSTNAME}}/SASLogon/oauth/authorize/response_type=code&client_id={{CLIENT_ID}}&redirect_uri={{URL_POSTFIXED_WITH_/callback}}
- This automagically presents the user with the same SAS Logon screen they see when accessing a native SAS Viya application. After they successfully authenticate, the URL redirects them back to our application (we included the
/callback
postfix in the previous step, so we see that in the redirected URL below) with an extra URL parameter like this (the code will of course be dynamic):https://{{YOUR_APPS_HOSTNAME}}/callback?code=pe3vyay7LJ
- So now, we substring off of the value of the user's current URL path and store that code in a variable, say code, temporarily. Next, the conditional logic will pick up the fact the user does have a code but isAuthenticated is false, so we kick off another function, feeding the code as an argument.
_ - This function makes the second call to SAS Logon Manager; this time to exchange the user's code for an OAuth token by making a request to:
https://{{VIYA_HOSTNAME}}/SASLogon/oauth/token/
_
With headers similar to the snippet below, again where anything in {{ }} is dynamically set:-H "Accept: application/json" -H "Content-Type: application/x-www-form-urlencoded" -u "{{CLIENT_ID}}:{{CLIENT_SECRET}}" -d "grant_type=authorization_code&code={{CODE}}"
- If the request is successful, we then call another function, which uses the js-cookie package in our case, to set a new piece of information in the user's cookie called access_token. This value gets pulled from the result of the previous request. At the same time, we update our isAuthenticated state variable to true.
_ - Now that the state has changed (isAuthenticated is true), the page gets re-rendered in typical React fashion. If our URL included
/callback
then we know the user came from SAS Logon, and our application logic redirects the user to the main/home
page of our application. Now we have the user's OAuth token stored in a cookie for them.
At this point, I think we've seen enough examples to know how to make downstream requests with the token by adding an Authorization header and setting it's value to Bearer {{TOKEN}}
, so I'll leave those pieces out from here. Don't forget each of those requests will be made for your application's individual logged-in users, so their unique identities will need to be able to access the endpoints/resources in SAS Viya through appropriate authorization rules.
On a similar note, and because this is getting really long 🙂 , it feels a little out-of-scope to include details on the refresh token logic. Just know, it follows a very similar pattern as above. If you've included refresh_token
in the authorized_grant_types
list when registering your client, then subsequent token requests made within the context of your client automatically include a refresh_token
property in the response you can parse out and use to get a new bearer token. The logout logic includes removing the user's cookie.
Just one more thing: if you have a multi-machine SAS Viya environment then using {{VIYA_HOSTNAME}}
might have been confusing. Check your Ansible inventory.ini
file and look for the machine specification for the hostgroup [CoreServices]
.
Thank you!
Whew. That was a doozy. Thanks for sticking with me through this post and the series! I hope this content gives you a better understanding of how to integrate your custom applications with SAS Viya and how to do it securely. Remember to evaluate each application's needs and intended use separately to choose the most appropriate type of scoping. Then use the guidance presented here to choose an OAuth flow so your application makes HTTP REST API calls into the SAS Viya endpoints and resources it needs. Feel free to reach out if you have any questions.
16 Comments
Hi Tara, congrats for this fantastic post series. It helps a lot on developing custom apps on Viya.
There is just one thing it's not so clear to me. When you register a client you must provide a client_secret. In your print screen you censored it because of security reasons, I guess.
Here is my doubt. If we have to use the client_secret to get the access_token, we have to write it into the js code and this makes the secret readable by everyone using browser developer tools. Is there a way to keep the client_secret hidden in a javascript front-end app or is the authorization_code flow so secure that it is not a problem to show the client secret in js app code?
Thank you very much!!!
Hey Claudio - thanks for the kind words. It should absolutely be treated like a sensitive piece of information. There are quite a few options, depending on your app framework and environment, for dealing with that securely. For example, you could store the secret in something like Azure Key Vault and make it available to your backend app as Azure discusses in this learning module. But exactly how you handle this depends on a number of factors like how and where you're hosting your app, for example.
Hi, other question: I would like to verify that a JWT token is indeed issued by our SAS Viya system.
I see that the token is signed with a RSASHA256 signature.
Where do I get the public key used to sign it in order to verify the signature?
I tried to explore /opt/sas/viya/config/etc/SASSecurityCertificateFramework without success.
Thanks!
Hi again Edo - unfortunately I don't have experience validating JWT tokens programmatically so I will have to do a little digging and will reach back out to you.
Solved! You can find the public key by doing a GET request to SASLogon/token_keys
Thanks, amazing!
To whom who may be interested, for example in python using the python-jose package, signature validation is straightforward:
from jose import jwt
jwt.decode(token,key,audience="{{CLIENT_ID}}")
where "token" is the token obtained from Viya, "key" is the json obtained with the GET request to SASLogon/token_keys, and audience is the intended client_id.
If it works, the token is authentic. If you get an error, the token has been spoofed or is not intended for you!
Hi, for authorization code flow, when configuring "redirect_uri": "https://notviyasite.mycompany.com/callback", everything works but after authentication the redirect is to "https://viya.mycompany.com/callback" which is not the intended url nor it exists.
This is some kind of security, right? How can I allow "notviyasite" to be a valid redirect domain?
Thanks!
Hi Edo - don't forget that the example for the auth code flow is all pseudo code to generalize for your platform/framework/language of choice, but it sounds like your programmatic call to /SASLogon/oauth/authorize might not have the proper redirect_uri URL parameter value in it. As long as the "redirect_uri" in the client registration and in your authorize request are the same, this will work.
Thanks, I dig a little deeper and I found out that the example I mentioned is indeed working, the issue is when I specify an explicit port.
Example:
- My application is running on https://myapp.mycompany.com:12345/
- Viya is running on https://viya.mycompany.com/
- A new client "myapp-client" is defined with: "redirect_uri": "https://myapp.mycompany.com:12345/callback"
- The user navigates in the app and gets redirected to https://viya.mycompany.com/SASLogon/oauth/authorize?response_type=code&client_id=myapp-client&redirect_uri=https://myapp.mycompany.com:12345/callback
- The user authenticates correctly on the SASLogon page
- The user gets redirected to https://viya.mycompany.com/callback which is wrong
Very weird behavior! It completely replaces either my application domain and my application port with Viya domain and default port (unspecified, so 443).
Instead, if I do: (note the absence of the port)
- My application is running on https://myapp.mycompany.com/
- Viya is running on https://viya.mycompany.com/
- A new client "myapp-client" is defined with: "redirect_uri": "https://myapp.mycompany.com/callback"
- The user navigates in the app and gets redirected to https://viya.mycompany.com/SASLogon/oauth/authorize?response_type=code&client_id=myapp-client&redirect_uri=https://myapp.mycompany.com/callback
- The user authenticates correctly on the SASLogon page
- The user gets redirected to https://myapp.mycompany.com/callback which is correct
So somehow the custom port is bothering it... Problem is that my application runs indeed on port 12345 as in the first example, and I cannot easily move it under 443.
Is this some security check?
Thank you very much!
Hi Edo - it's hard to say what might be going on there but I believe it is specific to your environment or perhaps networking? I can tell you that without a doubt our application that was the basis of this user-scoped access example was running on a specific port and I registered that to SAS Logon in the `redirect_uri` field of the Client I created. User redirects to and from our app as I outlined them in this post worked as expected with our app running on a specific port.
Hi Tara, I read your posts and Mike's SGF paper https://www.sas.com/content/dam/SAS/support/en/sas-global-forum-proceedings/2018/1737-2018.pdf
I am not sure how to interpret the following: see the #
"scope": ["openid", "*"],
"resource_ids": ["none"], # it means I cannot interact with any resources? by default blocked, forbidden
"authorities": ["uaa.admin"], # meaning?
in
curl -sk -X POST "${OAUTH_URL}/clients" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OAUTH_TOKEN" \
-d '{
"client_id": "app4",
"client_secret": "secret",
"scope": ["openid", "*"],
"resource_ids": ["none"],
"authorities": ["uaa.admin"],
"authorized_grant_types": ["password","refresh_token"],
"access_token_validity": 600
}'
Thank you for your extra clarification.
Hi Bogdan - using the value of "none" for the
resource_ids
property is fine, and it's the default so you can leave it out of your POST here. Our authorization systems are not relying on this property to determine your access. In the example you give, using thepassword
OAuth flow and grant type, that is determined with thescopes
property instead. The way you've specified it, your client would allow individual users access to anything they have access to normally, if they'd logged directly into one of the UIs. Instead of the "*" in the list, you can specify a Group or Custom Group instead and then your users would only assume the rights given to that group via any Authorization Rules with that group as the Principal. Theauthorities
property does the same thing for for clients that were registered with theclient_credentials
grant type which is what I cover in the "General-scoped access tokens" section of this post. Lastly, I would be remiss if I didn't make sure you're aware that thepassword
OAuth flow has user credential combinations flowing over the wire between your client and SAS Viya. Even if SAS Viya is secured over HTTPS, utilizing one of the flows covered in this post would be much more secure. If you need individual user-scoped access for your client, take a look at theauthorization_code
flow.
Congrats Tara, it's an amazing source of inspiration! I'm building a custom webapp using Django and SAS Viya API. I collected user projects, but I don't understand how to link them in order to redirect the user into the Model Studio in Viya, when he clicks on a specific project. Do you know how to do it? I guess to miss something in the api documentation. Thank you so much, congrats again!
Hi Alessandro, thanks for your comment. We have a /links endpoint that's responsible for generating custom links to specific pieces of content but it's current version only supports linking to SAS Visual Analytics reports and to the SAS BI mobile application. You could link users directly to the /SASModelStudio application which, from a normal user's (non SAS Administrators group member) view, will only display projects that he/she owns or that have been shared with them. If you'd like to email me your use case and company information I'd be happy to put in a feature request for you. Tara.StClair@sas.com
Tara this series is fantastic. Some great insights into best practices when creating Apps on SAS Viya.
Thank you Allan; I appreciate the read and the kind words.