Technical Articles
SAP BTP Security: How to use REST API of XSUAA to programmatically manage security artifacts
The XSUAA service of SAP BTP offers a REST API which allows to programmatically handle security artifacts like Roles, Role Collections, Users etc.
This blog post shows how to use it and provides a simple example.
Used technologies:
SAP Business Technology Platform Cloud Foundry environment, SAP Authorization and Trust Management (XSUAA) service, Node.js.
Quicklinks:
Quick Guide
Sample Code
Why do we need to programmatically handle security artifacts?
It allows to avoid many clicks in the cockpit by automating the configuration tasks. Scripts can be written in any language.
Basically, the only thing we need to understand:
We need to create an instance of XSUAA service with service plan apiaccess
The rest is REST.
I mean, managing the security artifacts is just using the REST API in standard way.
The required data – URL and credentials – can be found in the binding info of the service instance.
So let’s introduce the required steps in a little tutorial.
We will create 2 instances of XSUAA to illustrate the difference between the 2 service plans.
And we create a little script to try out the REST API of XSUAA.
Prerequisites
To follow this tutorial we need access to an account of SAP Business Technology Platform (SAP BTP).
We need sufficient permission to create artifacts.
This sample is written in Node.js, however, it can be used as template for any other language.
Content
1. Create first instance of XSUAA
2. Create second instance of XSUAA
3. Create Script
4. Run Script
Preparation
We create a project folder containing 3 files
C:\xsuaaRest
package.json
script.js
xs-security.json
In my example:
The content of the files can be copied from the appendix.
1. Create first instance of XSUAA
First of all, we create a “normal” instance of XSUAA.
This one is meant to simulate a running application which requires a certain scope.
As such, an admin needs to create a role collection which can then be assigned to an end-user.
This instance of XSUAA has the “normal” service plan “application” and declares a scope and a role-template:
xs-security.json
{
"xsappname": "testxsappname",
"scopes": [{
"name": "$XSAPPNAME.scopefortest"
}],
"role-templates": [{
"name": "TestRoleTemplate",
"default-role-name": "TestRole",
"scope-references": ["$XSAPPNAME.scopefortest"]
To create the instance, we jump into our project folder and execute
cf cs xsuaa application testXsuaa -c xs-security.json
Afterwards we can verify in the SAP BTP Cockpit that the new Role Template has appeared:
Subaccount -> Security -> Roles
Now we want to use the REST API to create a Role Collection and assign the default role which we’ve just created.
2. Create second instance of XSUAA
To use the REST API, we need to create a second instance of XSUAA.
We need a second instance, because the first one doesn’t provide access to the REST API of XSUAA.
Let’s learn about the service offering XSUAA:
cf marketplace -e xsuaa
We can see the service plans and we can see the service plan “apiaccess” which we might have seen before but never understood:
To create an instance with this service plan, we don’t need parameters, so no need of a second xs-security.json file.
We can just go ahead and run the following command:
cf cs xsuaa apiaccess apiXsuaa
We won’t create an application bound to this instance.
As such, to get a hold of the credentials, we need a service key:
cf csk apiXsuaa sk
To view the content of the service key:
cf service-key apiXsuaa sk
The result looks like this:
We can see the interesting property:
"apiurl": https://api.authentication.us10.hana.ondemand.com
This is the base URL of the REST API of XSUAA.
The API is protected with OAuth, so we need to fetch a JWT token.
To do so, we get the required info as well in the service key:
url and clientid and clientsecret.
3. Create Script
The script is very basic and meant to get you started and ready for further enhancements.
We’re using 2 helper libraries to make the code shorter:
@sap/xssec for fetching the JWT token
axios for executing the HTTP request
First thing we need to do is to copy the credentials from the command line to the script:
const CREDENTIALS = {
"apiurl": "https://api.authentication.us10.hana.ondemand.com",
"clientid": . . .
COPY YOUR SERVICE KEY HERE
. . .
This is required by the xssec library. Not all properties are required, but we don’t need to bother about it, just copy and paste.
The constants ROLE_TEMPLATE_NAME and ROLE_NAME are copied from the xs-security.json file and you’d need to adapt them in case you’re using different values.
const API_PATH = "/sap/rest/authorization/v2"
const API_FULL_URL = CREDENTIALS.apiurl + API_PATH
const ROLE_TEMPLATE_NAME = "TestRoleTemplate"
The API_PATH is copied from the documentation.
The script performs the following 3 steps:
1. Fetch JWT Token
2. Retrieve role template
3. Create role collection
async function runScript(){
const jwtToken = await _fetchJwtToken()
const roleTemplate = await _findRoleTemplate(ROLE_TEMPLATE_NAME, jwtToken)
await _createRoleCollection('TestRoleCollection', roleTemplate, jwtToken)
Let’s have a closer look at the steps:
1. Fetch JWT Token
As mentioned, the REST API is protected with OAuth, so to use it, we need to first fetch a JWT token.
The supported OAuth flow is client credentials.
To make life easy, we’re using the security library provided by SAP, it offers a convenience method:
xssec.requests.requestClientCredentialsToken(null, CREDENTIALS, null, null, (err, token)=>{
We have to pass the credentials object which we’ve copied from the service key.
The library will extract the necessary information from there.
2. Retrieve role template
We need the role template object in order to use it when creating the role collection. This is required because we want to assign the role template to the role collection during creation.
Although we have the information about the role template in the xs-security.json file, there’s one detail missing:
In order to identify the role template, the full xsappname is required.
Yes, we provided the xsappname in the xs-security.json file, but this is not the full truth.
When creating the service instance, the xsappname is enriched with some generated hash.
In my example it looks like this:
testxsappname!t97352
This is what we need and this is the reason why we need to search for it.
We can find it in the cockpit, along with the role, but we can also retrieve it programmatically.
To do so, we fetch all existing role templates with API call
const allTemplates = await _getRoleTemplates(jwtToken)
Then we find the correct one in the list by comparing the role template name which we take from the xs-security.json file:
const foundTemplate = allTemplates.find(template => {
return template.name.includes(templateName)
Once we have retrieved the role template object, we can access the corresponding full xsappname via the property appId:
"roleTemplateAppId": roleTemplate.appId,
3. Create role collection
To create a role collection, we use the /rolecollections endpoint and send a POST request with the fetched JWT token in the header.
The URL is composed by the base URL (copied from service key), the path of the API (from documentation) the endpoint (from docu).
We pass the desired data in the request body.
The data contains a role collection name of our choice,
some useless description and
the information about the role which should be contained in this role collection.
async function _createRoleCollection(name, roleTemplate, jwtToken){
const options = {
method: 'POST',
url: API_FULL_URL + '/rolecollections',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'bearer ' + jwtToken
},
data:{
'name': name,
'description': 'Generated programmatically',
"roleReferences": [{
"roleTemplateAppId": roleTemplate.appId,
"roleTemplateName": roleTemplate.name,
"name": roleTemplate['default-role-name']
}]
}
}
const response = await axios(options);
4. Run Script
So far, we have everything in place:
Created instance of XSUAA which created a role template.
Created instance of XSUAA which allows using API.
Created script to call the API, along with package.json.
Opened the BTP cockpit.
Now we’re ready to run the script from our local laptop.
However, before we can execute the script, we need to download the required libraries.
To do so, we open the command shell, jump into our project folder and execute
npm install
This command creates a node_modules folder and copies all dependencies into it.
Now we run the script with command
node script.js
It should print status 200 and exit without exceptions.
The result can be viewed in the SAP BTP cloud cockpit:
We navigate to subaccount -> Security -> Role Collections
There we can see the newly generated role collection along with the added role.
5. Clean up
For your convenience, find below the commands to delete the artifacts which we created above:
cf ds testXsuaa -f
cf dsk apiXsuaa sk -f
cf ds apiXsuaa -f
Summary
In this blog post we’ve learned how to use the REST API of XSUAA to programmatically handle security artifacts.
At least, we’ve done a first step which should be good to continue investigating.
The REST API is easy to use, with the help of the documentation.
The only little piece of information that might be helpful in today’s blog post:
Remember the service plan apiaccess.
It might be confusing at first sight, that we already have an XSUAA instance, but cannot use it to call APIs.
But indeed: we need to create a separate instance of XSUAA with service plan apiaccess.
From there we can obtain the URL and the credentials for the APIs.
Quick Guide
In order to use the REST API of XSUAA, create an instance with service plan apiaccess:
cf cs xsuaa apiaccess myName
Create a service key to access
-> the API URL
-> and the properties to fetch JWT token: OAuth URL, clientid and clientsecret
To use the API: refer to the documentation for the path that needs to be appended to the API base URL.
Example: https://api.authentication.us10.hana.ondemand.com/sap/rest/authorization/v2/rolecollections
The documentation also informs about the endpoints and supported HTTP methods and request / response data.
See SAP API Business Hub main entry: https://api.sap.com/package/authtrustmgmnt/overview
More Info
Some example URL:
Get roles (Authorization API):
https://api.authentication.us10.hana.ondemand.com/sap/rest/authorization/v2/roles
Get role collections with or without content:
https://api.authentication.us10.hana.ondemand.com/sap/rest/authorization/v2/rolecollections?showUsers=false&showRoles=true&showGroups=false
Get instances of XSUAA service:
https://api.authentication.us10.hana.ondemand.com/sap/rest/authorization/v2/apps
Get users (Platform API):
https://api.authentication.us10.hana.ondemand.com/Users
Get Identity Providers (Trust Configuration API):
https://api.authentication.us10.hana.ondemand.com/sap//sap/rest/identity-providers
Alternative: the BTP CLI:
See Docu: Managing Users and Their Authorizations Using the btp CLI
Links
Documentation main entry point
API reference on SAP API Business Hub: https://api.sap.com/package/authtrustmgmnt/overview
The REST API of XSUAA used in this blog post: https://api.sap.com/api/AuthorizationAPI/overview
Appendix: Sample Code
xs-security.json
{
"xsappname": "testxsappname",
"tenant-mode": "dedicated",
"scopes": [{
"name": "$XSAPPNAME.scopefortest"
}
],
"role-templates": [{
"name": "TestRoleTemplate",
"default-role-name": "TestRole",
"description": "Role template for end users",
"scope-references": ["$XSAPPNAME.scopefortest"]
}
]
}
package.json
{
"dependencies": {
"@sap/xssec": "^3.2.13",
"axios": "^1.1.2"
}
}
script.js
const axios = require('axios')
const xssec = require('@sap/xssec')
const CREDENTIALS = {
"apiurl": "https://api.authentication.us10.hana.ondemand.com",
"clientid": "sb-na-efa-4eaff138a021!a97352",
"clientsecret": "9ee0714d-a05b-4E=",
"identityzone": "mytrial",
"identityzoneid": "63d7e6c5-5ff8-4979-a69b",
"subaccountid": "63d7e6c5-5ff8-4979-a69b",
"tenantid": "63d7e6c5-5ff8-4979-a69b",
"tenantmode": "dedicated",
"uaadomain": "authentication.us10.hana.ondemand.com",
"url": "https://mytrial.authentication.us10.hana.ondemand.com",
"verificationkey": "-----BEGIN PUBLIC KEY-----\nMIIBIjA...wIDAQAB\n-----END PUBLIC KEY-----",
"xsappname": "na-a936b84e-02dc-4f85-9efa",
"zoneid": "63d7e6c5-5ff8-4979-a69b"
}
const API_PATH = "/sap/rest/authorization/v2"
const API_FULL_URL = CREDENTIALS.apiurl + API_PATH
const ROLE_TEMPLATE_NAME = "TestRoleTemplate"
// The script
async function runScript(){
console.log("START SCRIPT...")
console.log("Fetching JWT token for XSUAA REST API")
const jwtToken = await _fetchJwtToken()
console.log(`Retrieve RoleTemplate object with name ${ROLE_TEMPLATE_NAME}`)
const roleTemplate = await _findRoleTemplate(ROLE_TEMPLATE_NAME, jwtToken)
console.log("Generate Role Collection")
await _createRoleCollection('TestRoleCollection', roleTemplate, jwtToken)
console.log("DONE")
}
// execute the script
runScript()
/* HELPER */
async function _fetchJwtToken (){
return new Promise ((resolve, reject) => {
xssec.requests.requestClientCredentialsToken(null, CREDENTIALS, null, null, (err, token)=>{
resolve(token)
})
})
}
// find the roleTemplate object which matches our given template name
async function _findRoleTemplate(templateName, jwtToken){
const allTemplates = await _getRoleTemplates(jwtToken)
const foundTemplate = allTemplates.find(template => {
return template.name.includes(templateName)
})
return foundTemplate
}
async function _getRoleTemplates(jwtToken){
const options = {
method: 'GET',
url: API_FULL_URL + '/apps/roletemplates',
headers: {
'Accept': 'application/json',
'Authorization': 'bearer ' + jwtToken
}
}
const response = await axios(options);
return response.data
}
async function _createRoleCollection(name, roleTemplate, jwtToken){
const options = {
method: 'POST',
url: API_FULL_URL + '/rolecollections',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'bearer ' + jwtToken
},
data:{
'name': name,
'description': 'Generated programmatically',
"roleReferences": [{
"roleTemplateAppId": roleTemplate.appId,
"roleTemplateName": roleTemplate.name,
"name": roleTemplate['default-role-name']
}]
}
}
const response = await axios(options);
console.log("Role Collection created with HTTP status: " + response.status)
}
Thanks for sharing these information about the specific XSUAA service type. This helps to automate a few things! I am thinking about creating role-collections within a pipeline step of my CI/CD Pipeline!
Really good input Carlos!
On the other hand it is required to highlight; in a shared responsibility matrix of different teams, this can result into confusion if the developer is able to handle user/role assignments. Just as a heads-up for us developers, loving to automate things everywhere 😉
Regards,
Cedric
Hi Cedric Heisel,
thanks for the comment and thanks for sharing your concern with the community, this is good practice, just like automating things 😉
Cheers,
Carlos
Thanks for sharing your experience of consuming the Authorization API, which is useful for some use cases I can already imagine.
I have some findings below:
I see that you implemented functions like "_getRoleTemplates" and "_createRoleCollection", which
Also, you created a convenient function "_fetchJwtToken" for fetching the service token by using the "xssec" lib.
In addition, you used a local variable for saving the "CREDENTIALS", containing sensitive information. I assume this is just for test cases and for productive environment, you will use the destination service on BTP. Then additional code for consuming the destination service is missing here.
Fortunately, all of above ("_getRoleTemplates", "_fetchJwtToken" and consuming destination service) are covered by the SAP Cloud SDK.
With the SAP Cloud SDK, you source code of "_getRoleTemplates" should look like:
The "myDestination" is a reference to the name of the destination you create on the BTP.
For "_createRoleCollection", you code should look like:
To use the "RoleCollectionsApi", all you need is a client that can be generated by using the SAP Cloud SDK, where you can find on the API Hub page:
https://api.sap.com/api/AuthorizationAPI/cloud-sdk/JavaScript
More detailed documentation regarding the OpenAPI (the Authorizaiton API is an OpenAPI service) support by the SDK can be found here:
To get support, please create an issue here.
Best regards,
Junjie
Thanks for this informative blog Carlos Roggan
I have one question here. Instead of coding everything (and storing credentials in file which could be a security risk), wouldn't it be better to connect to the APIs using destination? We can create an XSUAA instance and create a destination using it.
Let me know your thoughts on this.
Thanks, Girish
Hello Girish Kumar Gupta ,
thanks for the useful comment.
I assume that yes, a destination can be used to hold any kind of URL and credentials.
A destination is basically used to externalize URLs and credentials, to avoid hard-coding, and easily transport to other landscapes.
On top, some destinations offer really powerful connectivity functionality and convenience.
I never use destinations in my samples, to better focus on the point under discussion.
I believe it makes it more clear to see what's being done.
All enterprise-grade stuff like error handling, clean code etc is ignored, to make sample small and clear.
But you can share your destination snippet in a comment, would be cool ! 😉
Kind Regards,
Carlos
Sure Carlos Roggan - I'll do that 🙂
Instead of the manual creation of service instances, I tried to move them to the MTA file, so that instances and keybindings are automated. Although it was successful post-deployment, the oData service was crashing.
I tried binding both XSUAA instances to the "app-srv" so that the client ID and secret ( of "apiaccess "instance) can be read from the environment.
It would be helpful to know how to automate the process creating the of XSUAA instances and key binding.
The original question can be found here Is it possible to configuring multiple XSUAA service plans application and apiaccess in MTA? | SAP Community
PS: In the MTA file, If we only declare the XSUAA service-plan "apiaccess" under the resources section and do not bind it to any module, then no service key will be generated during deployment.
Best Regards,
Harsha
Hello Sriharsha C R
thanks for the interesting comment.
Unfortunately, I'm not familiar with mta, but it would be interesting if you could post the result of your investigation here
Kind Regards,
Carlos
Hello Carlos Roggan
I was able to find a note (2760424), that states the service instance of "apiaccess" plan cannot be subscribed, it's quite old so I have created an internal incident on this note, to check if there are any further updates on this.
Best Regards,
Harsha
Hello Sriharsha C R
Thanks for contributing this information and this note, which is interesting anyways
Kind Regards,
Carlos
Hi Carlos,
I followed all steps but getting CORS issue and 401 error.
I tried it for Fiori app in SAPUI5 code, and tried to call below url to get role collection's assigned users. https://api.authentication.eu10.hana.ondemand.com/sap/rest/authorization/v2/rolecollections/xxxxxcentexxx?withUsers=true
For that first I call below service to get access token and type.
https://shg-housing-property-fsm-eval.authentication.eu10.hana.ondemand.com/oauth/token?grant_type=client_credentials"
By passing clientid and clientsecret of service key, we able to get access token and type, and same we passed as parameter with REST service, via ajax call. https://api.authentication.eu10.hana.ondemand.com/sap/rest/authorization/v2/rolecollections/xxxxxcentexxx?withUsers=true but it is giving CORS issue and 401 error. below is the code.
Kindly suggest how to fix this issue and get role collection detail.
Thanks
Divyanshu
Instead of hard coding Client ID and Secret, can we use BTP destination created to the Authorization and Trust management service?
I haven't tried it, but I'm sure you can always use destination.
It is just a kind of externalization with convenience feature of fetching the JWT token for you.
Hey Carlos. Thanks for this.
Quick one on the /Users endpoint. What if my UAA is tenant aware ? Having said so , my subscribers do not have CF space allocated to create instance of apiaccess. Any option to make my provider su account api access service tenant aware ?
Hello Sreehari V Pillai ,
from my understanding, to create an OAuth client in a subaccount, you need resources, so you need a space.
But have you tried to bind your provider app to apiaccess-instance, by providing binding-name?
Then access it in subscription?
You may also try posting a question in the answers.sap community to let the experts comment on the question
Kind Regards,
Carlos
Tried and failed to bind . As its not shared, its not accessible from subscription . Alternate option I see is to enable CF in my subscribing accounts ( I wouldn't have plenty ) and keep minimum resources there . And make the destination management to toggle the target destinations. I posted a q already , hoping to get some response. I will meanwhile open a SAP ticket too .
+ tenantmode : shared is not possible while creating or updating apiaccess instance. So no donut for us there