Technical Articles
How to call protected app | from external app || as external user ||| with scope
or:
Calling
oauth & scope protected app
from other app
using client credentials
and as external user
or:
How to assign scope
to foreign app
and external user
The problem I’d like to address with this tutorial is the following:
Scenario: In SAP Cloud Platform, we have app 1 and app 2:
App1: Provides API protected with OAuth2 requires scope |
< – – – – > | App 2: The client app that calls app 1 |
The problem:
To call the API, the cliet has to do the OAuth flow programmatically, that’s no issue.
BUT:
The JWT token doesn’t contain the scope which is required by app 1
How to assign a role to an app???
Quicklinks:
Quick Guide
Sample Code
Prerequisites
- We choose Node.js to write the little apps
So you need to have Node.js installed on your machine
See here for little help, no need to be very familiar with node.js development - Access to SAP Cloud Platform, Trial account (see here)
- Some experience with app development, app deployment
- Command Line Client for Cloud Foundry (see here) is not a must, but useful
Overview
1 Create API Provider App
2 Call API with human user
3 Create Client App and call API
Appendix: All Sample Project Files
Preparation: Create Project Structure
Create base project folder for our 2 apps:
C:\tmp_app2app
Inside, we create 2 root folders for our apps, and in addition, a third folder for our optional human test:
apiproviderapp
clientapp
humanuser
Step 1: Create API Provider App
First we create our app 1, the app which provides protected API, we call it providerapp
To protect our app, we use the XSUAA (Extended User Authentication and Authorization) service in SAP Cloud Platform
1.1. Create XSUAA instance
To create an instance of xsuaa, we define first the configuration parameters, which are required during instance creation.
The parameters can be copy&pasted directly in the cloud cockpit, but it is better to store it in a descriptor file
Jump into C:\tmp_app2app\apiproviderapp
Create a file called xs-security.json
Note:
The name is not relevant, you can use any name of your choice
But xs-security.json is the usual name
Copy the following content:
{
"xsappname" : "xsappforproviderapp",
"tenant-mode" : "dedicated",
"scopes": [{
"name": "$XSAPPNAME.scopeforproviderapp",
"granted-apps" : [ "$XSAPPNAME(application,xsappforhumanuser)"],
"grant-as-authority-to-apps" : [ "$XSAPPNAME(application, xsappforclientapp)"]
}],
"role-templates": [ {
"name" : "TheProviderRoleTemplate",
"default-role-name" : "TheRoleForProviderApp",
"scope-references" : ["$XSAPPNAME.scopeforproviderapp"]
}]
}
With this configuration, we define a scope and new role in SAP Cloud Platform
That role can be assigned to users
Later, we will bind our app to this instance of xsuaa, and with the help of xsuaa, we’ll ensure that the API can only be invoked if the caller has that scope
Note:
The role-templates section is only required for human users.
If you’re planning to skip the optional chapter 2, you can remove that part from the json
Note:
The value of xsappname property must be unique in the identityzone
Note:
Prefixing the scope name with the variable $XSAPPNAME makes the scope name unique and accessible
The variable will be resolved by the platform, we will see it below
Let’s continue
Based on the xs-security.json file, we can create the instance of xsuaa service, with name xsuaaforprovider
In command prompt, folder apiproviderapp, run the following command
cf cs xsuaa application xsuaaforprovider -c xs-security.json
The syntax:
create service instance of service <xsuaa> and service plan <application>. Then the desired <name> of the instance and the <configuration> params as file
You can also use the cloud cockpit and use the xs-security.json file in the creation wizard
After the service instance is created, we can use it in our app.
1.2. Create app
Our app is a silly little app which exposes a silly REST endpoint
Nevertheless, the app absolutely wants to protect the endpoint with OAuth2
That’s tedious enough….but:
Furthermore, the app wants that whoever is silly enough to call the endpoint, he must have a role (scope) assigned
Installation
In folder C:\tmp_app2app\apiproviderapp create a file called package.json
With following content:
{
"main": "server.js",
"dependencies": {
"@sap/xsenv": "latest",
"@sap/xssec": "latest",
"express": "^4.16.3",
"passport": "^0.4.1"
}
}
The dependencies are required to start a server and to add security
To install the dependencies, open command prompt, jump into folder apiproviderapp and execute the command npm install
See here:
Implementation
In the folder C:\tmp_app2app\apiproviderapp create a file called server.js
Copy the full content which can be found in the appendix section
Explanation:
Our little app exposes a REST endpoint which just returns a message.
app.get('/getData', function(req, res){
The authorization header, which contains a JWT token, is analyzed to view if the required scope is present.
const MY_SCOPE = xsuaaCredentials.xsappname + '.scopeforproviderapp'// scope name copied from xs-security.json
if(req.authInfo.checkScope(MY_SCOPE)){
res.send('The endpoint was properly ca
The check for valid JWT token is done by passport module
app.use(passport.initialize());
app.use(passport.authenticate('JWT', { session: false }));
In addition, just for our convenience, we add a middleware which is invoked prior to all other code.
app.use(jwtLogger)
This middleware just decodes and prints the relevant content of the JWT token
function jwtLogger(req, res, next) {
const jwtToken = readJwt(req)
console.log('===> JWT: scopes: ' + jwtToken.scope);
1.3. Deploy
In our app folder C:\tmp_app2app\apiproviderapp we create a file with name manifest.yml
The content:
---
applications:
- name: providerapp
memory: 128M
buildpacks:
- nodejs_buildpack
services:
- xsuaaforprovider
Explanation:
We define a name for our app in the cloud
We declare a dependency to the instance of xsuaa service which we created earlier
To deploy, we run the following command in the same folder apiproviderapp
cf push
(requires the Command Line Client, see prerequisites)
Step: 2 Call API with human user
After deployment, we can invoke the endpoint of our app in the browser.
https://providerapp.cfapps.eu10.hana.ondemand.com/getData
Result: 401 Unauthorized
We check the logs with cf logs providerapp –recent
Result: No relevant info in the log
Our endpoint implementation is not even reached.
Reason: we haven’t sent a JWT token
Consequence: We need to do the OAuth flow
How to call an OAuth protected service that requires OAuth flow?
I’ve described it in detail in this blog
So here I’m only giving short description
How do we get a JWT token?
We have to use xsuaa
2.1. Create xsuaa
So we create a new instance of xsuaa, with name xsuaaforhuman
Why create a new instance of xsuaa, why not use the one we created above?
That would be possible, but we’re assuming a scenario where the user is not the same person who created the API provider app
In folder C:\tmp_app2app\humanuser we create a file xs-security.json with following content
{
"xsappname" : "xsappforhumanuser",
"tenant-mode" : "dedicated",
"foreign-scope-references": [
"$XSAPPNAME(application,xsappforproviderapp).scopeforproviderapp"
]
}
Explanation:
The syntax is:
<service plan>, <xsappnameofprovidingxssecurityfile>.<scopename>
Note:
Make sure not to use e.g. the instancename, or applicationname, or the rolename
Note:
Alternatively, the following statements are valid as well:
"$XSAPPNAME(application,b2d58db5-635f-44a2-97d6-8b23be70fd5c,xsappforproviderapp).scopeforproviderapp"
Above one contains the subaccount id
Below one is using the resolved xsappname instead of the variable
"xsappforproviderapp!t45671.scopeforproviderapp"
Now, to create the instance of XSUAA, open command prompt, step into folder
C:\tmp_app2app\humanuser and run
cf cs xsuaa application xsuaaforhuman -c xs-security.json
Or use the cockpit to create an instance with name xsuaaforhuman
Since we’re a human, we cannot bind ourself to the service.
As such, we need to create a service key.
That can be done in the cockpit, in the details screen of the service instance
Or on command line:
cf csk xsuaaforhuman serviceKeyForHuman
Syntax:
Create service key for instance and give the desired name
Then we have to view the content of the service key.
In the cockpit it is visible, on command line run:
cf service-key xsuaaforhuman serviceKeyForHuman
Now, from the service key, take a note of the following properties:
“clientid”: “sb-xsappforhumanuser!t12345”,
“clientsecret”: “CLLaQTTjfG1i9PGrBrRLaIigpjo=”,
“url”: “https://123abc456trial.authentication.eu10.hana.ondemand.com”,
2.2. Call API with REST client
Now use a REST client tool which supports OAuth to fetch a JWT token and call our REST endpoint
Support for OAuth means that the tool does an additional request (to fetch the token) before it executes the actual desired request to the endpoint
In the details dialog for OAuth2, enter the following details:
Authorization | OAuth2 |
Grant Type | Password |
URL | append oauth/token to the url property e.g. …authentication…ondemand.com/oauth/token |
User / Password | your own Cloud Platform user/password |
ClientID / secret | values from properties from service key |
The screenshot shows the configuration in postman:
Press “request”, then “use token”, then “send”
Result: 403 Forbidden
AGAIN!
But this time, in the logs, we can see that the JWT has been sent, but it doesn’t contain the required scope
Reason:
As a foreign user we want access to foreign scope, as declared in our xsuaa descriptor
But wanting it is not enough.
The providing app has to allow it
That’s fair enough…
2.3. Grant the scope
As a provider app, I can allow a foreign user to use my scope
What is a “foreign user”?
That’s somebody who uses a different instance of XSUAA
To allow, I have to GRANT my scope to the foreign user
The foreign user is represented by the instance of xsuaa which he is using
And the instance of xsuaa is represented by the property xsappname
As such, the provider app grants the scope to the foreign xsappname
xsappname is called “app”, see below.
As such, the statement is:
"granted-apps" : [ "$XSAPPNAME(application,xsappforhumanuser)"]
Syntax:
<service-plan>,<xs-app-name>
As a provider app, I have to add this statement to my xsuaa descriptor when creating the instance of xsuaa
The excerpt of xs-security.json
"xsappname" : "xsappforproviderapp",
"scopes": [{
"name": "$XSAPPNAME.scopeforproviderapp",
"granted-apps" : [ "$XSAPPNAME(application,xsappforhumanuser)"]
}],
...
See appendix for full file content
In our tutorial, we’ve already created the xsuaa instance (providerapp), so now, we have to “update” the instance, to make the changed parameters effective
Updating a service instance can be done in the cockpit – or in command prompt:
cf update-service xsuaaforprovider -c xs-security.json
If you don’t use the command line, you have to delete the xsuaa instance in the cockpit, then re-create it with new parameters
After updating the xsuaa of the provider app, we have a nice relation between the 2 xsuaas:
The provider does “grant”, the humanuser does “reference” it
We can try the request with postman again:
fetch new token, use it, then send the request
But we still get forbidden and in the log we still don’t see the scope in our JWT token
One more step is required:
The human user needs to have the corresponding role assigned
Remember: the api provider app not only defined a scope (with grant), but also defined a role-template, based on the scope.
Like that, the role can be assigned to human users
2.4. Assign Role to user
In Cloud Cockpit, go to subaccount
On the left menu: Security->Role Collections
Create a Role Collection with a name of your choice
Click the new Role Collection, then “Add”
In the dialog, choose the application identifier, e.g.
xsappforproviderapp!t12345
Note:
Remember?
This is the value we gave for property xsappname in the xs-security.json file
The platform has generated the full name.
That’s why we couldn’t hardcode it in the application code of our provider app
After adding the role to the role collection, we have to assign the role collection to our user
In subaccount, Security->Trust Configuration click on the active entry (“sap.default”)
Enter the e-mail address then click Show Assignments, then Assign Role Collection
Choose your new Role Collection, close the dialog
Now your user owns that role (the scope defined by the xsuaa bound to providerapp)
Call endpoint
In postman, request new token, use it, send the request to the endpoint of providerapp
Finally, the request is successful
We see in the log that the scope was contained in the JWT token
Recap
Scenario:
There’s a provider app which requires a scope
And there’s a human foreigner which wants to call that app
The human user is a foreigner because he has his own xsuaa
To enable this scenario:
1. the provider app has to grant the scope explicitly to the foreign xsuaa
2. the foreign xsuaa has to explicitly reference the granted scope
3. the foreign human user needs the (greanted) role assigned to him
Note:
Instead of postman, we could have used an app router. See here
Diagram
Step 3: Create Client App and call API
Now we’re coming to the actual purpose of this tutorial:
we want to call the endpoint of the provider app programmatically, from a second app
This app as well needs its own instance of xsuaa
In the previous, optional chapter, we’ve learned that the 2 involved xsuaa instances have to reference each other
However, the mechanism is different for client apps than for human users.
Let’s see
3.1. Create xsuaa for client app
In foder C:\tmp_app2app\clientapp we create a new xs-security.json file with content:
{
"xsappname" : "xsappforclientapp",
"tenant-mode" : "dedicated",
"authorities":["$ACCEPT_GRANTED_AUTHORITIES"]
}
The relevant line here is the “authorities” property.
This means, with other words, that the client app wants to use the authorities which are granted by providing app
Note:
This statement can be refined to accept only certain scope:
"authorities":["$XSAPPNAME(application,xsappforproviderapp).scopeforproviderapp"]
Or this variant works as well, easier to read, with resolved variable (but don’t use it)
"authorities":["xsappforproviderapp!t45671.scopeforproviderapp"]
Note:
Foreign-scope-reference doesn’t work
That’s only used in user-centric scenarios
To create this service instance, open command prompt and make sure to step into in the clientapp folder
Then execute the following command
cf cs xsuaa application xsuaaforclient -c xs-security.json
Note:
We don’t need to create service key, because we’re going to bind the client app to this xsuaa instance
3.2. Update the providing xsuaa
Now we need to change the xsuaa descriptor of the providing app.
In order to allow a scope to a foreign app (not user), the following statement is used:
"grant-as-authority-to-apps" : [ "$XSAPPNAME(application, xsappforclientapp)"]
As we can see, here the scope is granted as “authority”
Syntax:
<service plan>, <xsappnameofusingapp>
Note:
Make sure to enter the correct xsappname of the xsuaa created above for the client
The relevant excerpt of file C:\tmp_app2app\apiproviderapp\xs-security.json:
"xsappname" : "xsappforproviderapp",
"scopes": [{
"name": "$XSAPPNAME.scopeforproviderapp",
"grant-as-authority-to-apps" : [
"$XSAPPNAME(application, xsappforclientapp)"
...
See appendix for full file content
After changing the xs-security.json file, don’t forget to call the “update-service” command
Open command prompt in folder C:\tmp_app2app\apiproviderapp and run
cf update-service xsuaaforprovider -c xs-security.json
After updating the xsuaa of the provider app, we have a nice relation between the 2 xsuaas:
The provider does “grant”, the client does “accept” it:
3.3. Create the client app
package.json
In folder C:\tmp_app2app\clientapp create a package.json file and copy the content from the appendix section
server.js
In folder C:\tmp_app2app\clientapp create a server.js file and copy the content from the appendix section
In the implementation, we do the following:
– access the OAuth credentials from the environmentThis is possible because the app is bound to the xsuaa service instance
It is required because we need the client id and secret and token-url
(what we used in postman request)
We need it in order to fetch the JWT token from xsuaa server
const VCAP_SERVICES = JSON.parse(process.env.VCAP_SERVICES)
const CREDENTIALS = VCAP_SERVICES.xsuaa[0].credentials
const OA_CLIENTID = CREDENTIALS.clientid;
– execute an https call to fetch the token
const fetchJwtToken = function() {
...
const options = {
Authorization: "Basic " + Buffer.from(OA_CLIENTID + ':' + OA_SECRET).toString("base64")
...
https.get(options, res => {
...
– use the token to call the endpoint of the provider app
const doCallEndpoint = function(){
...
const options = {
host: 'providerapp.cfapps.eu10.hana.ondemand.com',
path: '/getData',
...
Authorization: 'Bearer ' + jwtToken
...
const req = https.request(options, (res) => {
– to make this procedure accessible in the cloud, we start a server and offer an endpoint
app.get('/trigger', function(req, res){
...
app.listen(process.env.PORT, ()=>{})
See appendix for full file content
3.4. Deploy the client app
In folder C:\tmp_app2app\clientapp create a manifest.yml file and copy the content from the appendix section
...
- name: clientapp
services:
- xsuaaforclient
In the manifest we declare a dependency to the xsuaa instance created above
Call provider app from client app
After deployment, we can invoke the endpoint of our client app in a browser
…fortunately, this endpoint is not protected at all…;-)
https://clientapp.cfapps.eu10.hana.ondemand.com/trigger
As a result, we get a success message
Recap
To enable communication between 2 apps, using different xsuaa, but in same subaccount:
1. in protected app (provider), the scope must be „granted“ to the client-xsuaa
2. in using app (client), the “authorities” property is required in xs-security.json
Note:
If you need a scenario where the apps are located in different subaccounts, refer next tutorial
Diagram
Summary
Thanks for reading this blog.
I hope it has helped to solve these tedious authorization-problems
We have covered 2 scenarios:
A) Protected app is called by external user:
The app is protected with OAuth and defines a scope and a role
The external user creates his own xsuaa instance to call that API
B) Protected app is called by client app
The app is protected with OAuth and defines a scope
The client app wants to call the API and somehow needs to have that scope
Note:
More information about JWT tokens can be found in this blog post
Troublemaking
- If the forbidden-error persists, then probably there is any typo in one of the xs-security.json files
- As mentioned above: make sure that you enter the value of the property xsappname (copy it from xs-security file), not instance name nor app name
- When executing the update-service command, make sure to step into the correct folder, such that you don’t e.g. update the provider-xsuaa with the content of the other client-xs-security
- To avoid such a copy&paste error, or error with history of cmd, you can choose different names for each xs-security.json file (e.g. xs-sec-provider.json, etc)
- Make sure that you’re running the scenario in the same subaccount
If you need 2 subaccounts, see the next tutorial - When using REST client, make sure to select the appropriate grant type. User credentials do work only if user has roles
- In case of user: didn’t forget to add role collection to IDP?
- If the problem persists: if you’re using old client lib, it might be necessary to add the trust variable to the env of your app
This is done by adding the following snippet to your manifest.yml
(Alternatively, it can be added in the cockpit or with set-env command. In these cases, make sure to restart or restage your app)
See this blog postfor detailed info.
his variable tells the xssec validator to accept foreign clientids and identitiyzones
manifest.yml:env: SAP_JWT_TRUST_ACL: > [ {"clientid":"*", "identityzone":"*"} ]
Quick Guide
App requires OAuth and scope
A) Protected app is called by external user:
We need:
1. “granted-apps” (in provider-app-xs-security.json)
2. “foreign-scope-references” (in human-user-xs-security.json)
3. Assign role to user
B) Protected app is called by client app
We need:
1. “grant-as-authority-to-apps” (in provider-app-xs-security.json)
2. “authorities” (in client-app-xs-security.json)
Links
Next Tutorial: same scenario, but with different subaccounts
Docu: SAP Help Portal
Useful info about security in this series: Blog series
OAuth 2.0
Understanding of OAuth for dummies like me.
JWT tokens info in this blog post
How to add custom property to JWT token
Little app router series
OAuth flow with REST client: here
Appendix: All Sample Project Files
For your convenience, see screenshot for overview about project structure
App 1: API Provider App
xs-security.json
{
"xsappname" : "xsappforproviderapp",
"tenant-mode" : "dedicated",
"scopes": [{
"name": "$XSAPPNAME.scopeforproviderapp",
"granted-apps" : [ "$XSAPPNAME(application,xsappforhumanuser)"],
"grant-as-authority-to-apps" : [ "$XSAPPNAME(application, xsappforclientapp)"]
}],
"role-templates": [ {
"name" : "TheProviderRoleTemplate",
"default-role-name" : "TheRoleForProviderApp",
"scope-references" : ["$XSAPPNAME.scopeforproviderapp"]
}]
}
package.json
{
"main": "server.js",
"dependencies": {
"@sap/xsenv": "latest",
"@sap/xssec": "latest",
"express": "^4.16.3",
"passport": "^0.4.1"
}
}
server.js
const express = require('express');
const passport = require('passport');
const xsenv = require('@sap/xsenv');
const JWTStrategy = require('@sap/xssec').JWTStrategy;
//configure passport
const xsuaaService = xsenv.getServices({ myXsuaa: { tag: 'xsuaa' }});
const xsuaaCredentials = xsuaaService.myXsuaa;
const jwtStrategy = new JWTStrategy(xsuaaCredentials)
// configure express server with authentication middleware
passport.use(jwtStrategy);
const app = express();
// Middleware to read JWT sent by client
function jwtLogger(req, res, next) {
console.log('===> Decoding auth header' )
const jwtToken = readJwt(req)
if(jwtToken){
console.log('===> JWT: audiences: ' + jwtToken.aud);
console.log('===> JWT: scopes: ' + jwtToken.scope);
console.log('===> JWT: client_id: ' + jwtToken.client_id);
}
next()
}
app.use(jwtLogger)
app.use(passport.initialize());
app.use(passport.authenticate('JWT', { session: false }));
// app endpoint with authorization check
app.get('/getData', function(req, res){
console.log('===> Endpoint has been reached. Now checking authorization')
const MY_SCOPE = xsuaaCredentials.xsappname + '.scopeforproviderapp'// scope name copied from xs-security.json
if(req.authInfo.checkScope(MY_SCOPE)){
res.send('The endpoint was properly called, role available, delivering data');
}else{
const jwtToken = readJwt(req)
const availableScopes = jwtToken ? jwtToken.scope : {}
return res.status(403).json({
error: 'Unauthorized',
message: `Missing required role: <scopeforproviderapp>. Available scopes: ${availableScopes}`
});
}
});
const readJwt = function(req){
const authHeader = req.headers.authorization;
if (authHeader){
const theJwtToken = authHeader.substring(7);
if(theJwtToken){
const jwtBase64Encoded = theJwtToken.split('.')[1];
if(jwtBase64Encoded){
const jwtDecoded = Buffer.from(jwtBase64Encoded, 'base64').toString('ascii');
return JSON.parse(jwtDecoded);
}
}
}
}
// start server
app.listen(process.env.PORT || 8080, () => {
console.log('Server running...')
})
manifest.yml
---
applications:
- name: providerapp
memory: 128M
buildpacks:
- nodejs_buildpack
services:
- xsuaaforprovider
env:
DEBUG: xssec:*
App 2: Client App
xs-security.json
{
"xsappname" : "xsappforclientapp",
"tenant-mode" : "dedicated",
"authorities":["$XSAPPNAME(application,xsappforproviderapp).scopeforproviderapp"]
}
package.json
{
"dependencies": {
"express": "^4.16.3"
}
}
server.js
const express = require('express')
const app = express()
const https = require('https');
// access credentials from environment variable (alternatively use xsenv)
const VCAP_SERVICES = JSON.parse(process.env.VCAP_SERVICES)
const CREDENTIALS = VCAP_SERVICES.xsuaa[0].credentials
//oauth
const OA_CLIENTID = CREDENTIALS.clientid;
const OA_SECRET = CREDENTIALS.clientsecret;
const OA_ENDPOINT = CREDENTIALS.url;
// endpoint of our client app
app.get('/trigger', function(req, res){
doCallEndpoint()
.then(()=>{
res.status(202).send('Successfully called remote endpoint.');
}).catch((error)=>{
console.log('Error occurred while calling REST endpoint ' + error)
res.status(500).send('Error while calling remote endpoint.');
})
});
// helper method to call the endpoint
const doCallEndpoint = function(){
return new Promise((resolve, reject) => {
return fetchJwtToken()
.then((jwtToken) => {
const options = {
host: 'providerapp.cfapps.eu10.hana.ondemand.com',
path: '/getData',
method: 'GET',
headers: {
Authorization: 'Bearer ' + jwtToken
}
}
const req = https.request(options, (res) => {
res.setEncoding('utf8')
const status = res.statusCode
if (status !== 200 && status !== 201) {
return reject(new Error(`Failed to call endpoint. Error: ${status} - ${res.statusMessage}`))
}
res.on('data', () => {
resolve()
})
});
req.on('error', (error) => {
return reject({error: error})
});
req.write('done')
req.end()
})
.catch((error) => {
reject(error)
})
})
}
// jwt token required for calling REST api
const fetchJwtToken = function() {
return new Promise ((resolve, reject) => {
const options = {
host: OA_ENDPOINT.replace('https://', ''),
path: '/oauth/token?grant_type=client_credentials&response_type=token',
headers: {
Authorization: "Basic " + Buffer.from(OA_CLIENTID + ':' + OA_SECRET).toString("base64")
}
}
https.get(options, res => {
res.setEncoding('utf8')
let response = ''
res.on('data', chunk => {
response += chunk
})
res.on('end', () => {
try {
const jwtToken = JSON.parse(response).access_token
resolve(jwtToken)
} catch (error) {
return reject(new Error('Error while fetching JWT token'))
}
})
})
.on("error", (error) => {
return reject({error: error})
});
})
}
// Start server
app.listen(process.env.PORT || 8080, ()=>{})
manifest.yml
---
applications:
- name: clientapp
memory: 128M
buildpacks:
- nodejs_buildpack
services:
- xsuaaforclient
Optional: Human User
xs-security.json
{
"xsappname" : "xsappforhumanuser",
"tenant-mode" : "dedicated",
"foreign-scope-references": ["$XSAPPNAME(application,xsappforproviderapp).scopeforproviderapp"]
}
Hi Carlos Roggan ,
Again a wonderful blog from you. Thanks for that.
I do have some queries.
Thanks,
Badhusha.
Hello Badhusha Akhthaar ,
Thank you soo much for this wonderful feedback 😉
About your questions:
* Why do you think it doesn't have any security?
It is protected with oauth (passport using JWT Strategy)
So it requires users to be authenticated
Furthermore it the users need to have a scope/role
If users/apps are bound to own xsuaa instance, then the foreign-scope-mechanism is required
The protected app allows only specific foreign xsuaa instance
Or did I miss any point?
* Why should you want to write an app which can only be used by ONE user?
Hum - I see.....maybe it is for your girlfriend only?
It is the responsibility of an admin to assign a role collection to a group of users, or to each single user individually. That depends on the used Identity Provider. In our Trial accounts there's the default SAP ID service.
So it would be up to you to bribe the admin such that he only assigns the role collection to your girlfriend
One more possibility would be to add an additional check in your app code:
After checking if the scope is contained in the JWT token, you can check the girliename, sorry, the username property. Manually. In the code. Probably not a common way
If you change girlfriends frequently, you can use e.g. a serverless function to provide you with the current username.
😉
Beg your pardon if you don't like my humor;-)
Cheers, Carlos 😉
Currently, I have deployed the client app and tried to call from the POSTMAN with NoAuth, It is returning success message(202-Accepted).
Is this is how the application works? I thought like when we call the application with Basic Auth, using the Mail id which we assigned earlier to a role, it will send the Success response, and for others, it will send Forbidden.
Or I have understood in the wrong way.?
I do like your humor ?
Thanks,
Badhusha.
Hello Badhusha Akhthaar ,
sorry for the late reply, strange that I didn't receive notification
pouh - glad that you didn't feel offended by my humor 😉
I think I misunderstood your question.
You're talking about the client app.
You're right: this one is NOT protected at all.
Correct.
But that's intended.
The topic of the blog is, to learn how to call a PROTECTED app.
From other app, external app.
The "other app" (client) can be protected as well, it doesn't make a difference for the topic of this blog.
But it makes it much easier to follow this blog, if it is not protected.
If you want to protect it:
you can add the same middleware like shown in the sample of the protected app
Then it is protected with oauth as well
To sign in with basic auth, you can use app router (see here)
Or you can try the native "basic auth" middleware of passport. But then you have to take care of validating the concrete user(s)
Hope it has been understandable, my english is too poor...
Don't hesitate to ask again 😉
Cheers,
Carlos
Wow great blog post Carlos! Thanks for taking your time for writing it.
I really like the way it is structured. I wished more of the SAP's official documentation would be like this. Most of the time the explanation are really confusing and there is lack of practical examples.
Hi Luisa Grigorescu ,
Thanks so much for the feedback…!!!
Glad to hear that it helps and encouraging for continuing this way…!
Cheers,
Carlos
Great Blog Carlos. Thanks for putting up such a detailed blog. Really helpful
I am trying to do a similar scenario and following the same you did for the client App.
However, I am stuck with the `403 Forbidden` error. When I check the logs, the scope is being passed correctly to the provider app but the user data is undefined.
Could this be a reason for the 403 Error?
logs
Would be really great if you could provide some insight on this.
Thanks in Advance,
Deepak
Hi Deepak Sahu ,
Thanks for your feedback, I'm glad that all the effort is helpful for somebody!
Regarding the user: depending on the scenario, this property is empty.
In case of client-credentials, which is the purpose of this blog, there's no user-login, so it is fine that this property remains empty
I see in your screenshot that the scope is contained in the token.
Maybe there's a mistake in the scope-check?
I mean, it is the provider-app which sends the 403-Error, due to manual implementation.
The passport library would send a 401 if the JWT token is not ok.
So you can debug your code and check if the check does correctly compare the incoming scope
Like this:
Hi https://people.sap.com/carlos.roggan,
Thanks for the nice and detailed explanation.
In my case, I have a clientapp (MTA app with UI5, html5 repo & app-router) and I have another providerapp (MTA with xsjs module & database module) and I would like to access providerapp endpoints from my client app.
Can you suggest me how can I access providerapp from my client UI5 app?
Do I need to make use of destination service? (I already tried this, created a destination for providerapp with providerapp's xsuaa client id & secret which is working, But I figured out this is wrong as client app user info is not propagated to providerapp level in this way, thus not possible to apply some database level authorizations)
Do I need to add a nodejs module to my client app MTA along with existing UI5 to implement as you explained? or Is there any other way to expose my providerapp to my clientapp to router my ui5 backend calls?
Thanks in advance for your suggestion.
Regards,
Suchen.
Hello Suchen Oguri ,
Thanks for your nice feedback
I wanted to suggest to follow-up in personal chat, but I've found your question here
I've tried an answer there, please check if it helps
Kind Regards,
Carlos
Hi Carlos,
What if:
Provider app xsuaa has "granted-app" for humanuser to access all APIsbut xsuaa plan is "broker",
I then create my own xsuaa of "application" plan with "foreign-scope-references" and use the url,client_id and secret to generate a token and access all provider app Apis?
Would it work if 2 xsuaa are of different plans?
Regards,
Kush Sharma
Regarding:
Now user can update service instance also in cockpit. Thank you SAP
Hi Vladimír Balko ,
thanks for pointing it out (I've adjusted) and thanks for the nice feedback 😉
Please keep commenting - it is helpful!
Cheers,
Carlos
Hi Carlos,
In my application. I have a approuter -- ui5 -- backend service setup. UI5 to backend is via ODATA api communication. While testing in local , I noticed the possibility of end user getting the cookie from browser - network tab and make post/delete operations on backend service from rest clients like postman.
I want to restrict end user calling the api directly and let only the UI call backend service.
I have tried giving only GET access to the user in the backend service , but the UI was not able to do POST operation on user request.
I'm new here, kind of lost on this. Please share your thoughts. Thanks in advance.
Hello chris pure ,
I'm a bit confused: your UI5 application is similar component like postman, just more convenient. I mean, it acts on behalf of the user. The user comes with account priviledges and roles, tailored to execute operations on backend data via OData service. The UI is just convenience.
Maybe it would help to distinguish roles, for protecting delete operations? Such that only admin can do deletion?
Otherwise you would need to do workarounds, like restricting the delete access only via client-credentials. You would define a scope for deletion - but not create role for that scope.
Then you would have to programmatically call that endpoint in your ui5-app, programmatically doing the oauth flow to obtain jwt token for client-credentials grant.
I'm sorry, I'm not architect, maybe there's a more elegant solution which I'm not aware of.
Kind Regards,
Carlos
Thank you Carlos, for your inputs. I will look in to more and get back to you.
One more question , for my application, we have approuter in front of ui5 application. Curious to know if wee need xs app.json in router and also in ui5/webapp folders ?
what is the difference between putting UI related files in approuter vs deploying UI app separately in html5 repository ? Thanks in advance.
Hello chris pure
the application router is an existing component which you can deploy standalone to Cloud Foundry. It is configured with xs-app.json.
As such, this file is part of approuter and it is required by approuter.
Approuter refuses to start if the file is not present in the expected location (BTW, approuter can be configured programmatically)
You can put approuter in front of a protected REST service, so the approuter does the OAuth flow for the user.
The most frequent use case is to put approuter in front of a UI5 application
And I guess in most cases there's only one approuter for one UI project.
(I mean, approuter not used standalone)
In this case, it is common behavior to use embedded approuter, together with ui5 app folder.
Like that, you can easily point to the webapp as starting point for the first route.
I went through little understanding process and I shared my learning here
Kind regards,
Carlos
Hi Carlos:
Thanks a lot for your blog post. It is life saving.
I would like to ask another scenario:
If the client application is not hosted in SAP BTP. Is there any solution to complete OAuth2 authentication flow and consume RESTful API hosted in SAP BTP?
Hi Chao Chen , sorry for the late reply, I must have missed the notification ;(
For remote access to a resource in BTP, you can create a service key for xsuaa service. This gives you the credentials to access that service without binding. It works for client credentials flow
Thanks, Carlos, for such a wonderful explanation! It has certainly enhanced my understanding about how xsuaa security works! And the added humour makes the learning enjoyable 😉
Thank you so much for this fantastic feedback Ashish Singh that is really helpful and encouraging 😉
Hi Carlos Roggan
If we have a CAP service which has roles and instance -based authorization on it.
And we need to access the same API from CPI tenant, how can we achieve the same?
Currently we are using client id and secret from cpi to access the cap service.
Thanks
Kanika
Hi Kanika Malhotra ,
I'm not familiar with CAP, but I think this blog post can help you:
https://blogs.sap.com/2022/03/15/403-forbidden-scope-missing-in-jwt-aaargh/
In brief, you need to assign the scope to your own xsuaa-instance.
Then enter clientid/secret in CPI security artifact.
When it fetches the JWT token, the token will contain the scope.
Hope this helps,
Kind Regards,
Carlos
Hi Carlos,
Thank you for this detailed blog. Very detailed. I am trying to implement something very similar to this, but both the apps are nodejs CDS apps. Trying to follow the this help
Two questions:
Thank you for help
Hi,
as I'm not familiar with CDS, I can only answer first question:
yes, of course, granting a scope is like assigning a role to a user, so it is an explicit statement.
However, you don't type the name of an application, you type the name of the oauth client, which is more flexible
Kind Regards,
Carlos
Thank you Carlos Roggan I will try your example as is and see how it works.
When you say add the name of "oauth client" What do you mean by that? I have a provider service? that is publishing the service. The "consumer app" is the client (oauth client). Hardcoding the oauth client itself. What would the name of the oauth client be ?
What I would like to build is a "service provider" in a space in a tenant. Any consumer in the same space and tenant are free to subscribe to the service and use it without changing the provider (unless a change is required in the provider functionality).
Sorry if I've confused you.
When you create an instance of xsuaa service, then you get the client id and client secret, right?
So this ID identifies an "OAuth client" that is registered at the "Authorization Server" (XSUAA server).
As such, we can use the term "oauth client" when talking about xsuaa-service-instance.
When you create an instance, you can configure it with xs-security.json file.
There, you can configure the property xsappname (otherwise it would be generated).
This is what I meant with the name of the oauth-client.
Kind Regards,
Carlos
Hi Carlos - this blog is a saver
Can I ask you a quick one ? My xsappname is dynamic - like this
Can I then , specify the app names in grant-as-authority , grant-as-apps app names in yaml file , so that this will be populated dynamically ?
Sree
Hi Sreehari V Pillai
Thank you for your feedback !
About your question, this is MTA-semantic and I'm not familiar with it, unfortunately.
As far as I understand, it allows for abstraction of deploy landscapes, resulting in a dynamic replacement of variables during deployment.
Which would mean that you could use the placeholders also for grant statement.
But this is just a guess
pls let us know once you've tried it
Cheers,
Carlos
Sure ill try this and update here . I am pessimistic here , as its not given in yaml but in xs-security.json . Ill be surprised if variables in json file is updated dynamically .
sree
maybe I didn't get your question properly: are you using 2 files, mta.yaml and xs-security.json? Or only the mta.yml as in your snippet?
During deploy, the mta-deployer will generate an instance of xsuaa, right?
And insert the variables-values
You can view the result after deployment, if you view the credentials of the generated xsuaa-instance
Sorry If I confused you.
I have a calling Application ( App A ) has an xs-security.json file. In its mta.yaml file , xsappname is set as : callingapp-${org}-${space}
when I deploy the app , mta.yaml variables shall be replaced and the xsuaa will use dynamically generated xsapp name
I do the same for called app as well . Now the problem is , I am maintaing the scope in xs-security.sjon , where I put called app's xsappname without runtime variables. I tried this now and deployment failed . So there must be an option to maintain scopes in yaml files , or to not to use dynamic app names in yaml .
I posted a separate question here
sree
ok. now I agree with your pessimism. I xs-security you cannot reuse mta-variables
As I'm not familiar with mta, I cannot answer, but I would assume that you can config options for scopes etc in mta-params-section
Do u have an example for this ? ( config param )
sorry, this was just a guess, I'm not familiar with mta
Hay .
Why don't we simply user "oAuth2 User toke exchange" authentication type in the destination ?
Create destination of called app using "user token exchange" . Mention it's client Id and secret.
we can avoid scope mapping in caller application . I tested it with 2 CAP applications and it works smooth
Sree
Hello Sreehari V Pillai ,
you can contact me directly via personal message to discuss further your scenario.
What I can say to help to clarify:
To get the scope, you need to add a GRANT statement to your xs-security (the mentioned authorities statement)
and in addition you add the user-token in order to add the user-info
A jwt token issued for one client cannot be accepted by any different client
So the scope must be granted beforehand
Grant scope must be always there, either as grant statement in the security-descriptor.
Or, what you're doing, adding the grant by assigning the role to a user
The descriptor-way is more convenient, because done once and valid for all users
Unfortunately, I don't find the time now to verify it, so it is just my strong guess
Thanks and kind regards,
Carlos
Perhaps , what you explained in the blog is different than the (simple) scenario I was trying to achieve .
Scenario
I have 2 micro services ( CAP is irrelevant here ). Order-App and Masterdata-App . Order-App has its own xsuaa instance. A user must have scope "order-create" to create an order from the frontend . To read the master data from Masterdata-App , the caller must have a scope "mdm-read" scope. Here , "Order-app" makes an http request to "Masterdata-App"'s Odata endpoint ( from nodejs ).
Solution
I created a destination in my sub-account "MasterData" . Type being "OAuth2userTokenExchange" .
from "Order-App" , I connect to this destination , propagating the token issued to the froentend by the xsuaa of "Order-app" ( thats where the user apparently logged in ) .
Destination service then generated another token for Masterdata-App for the logged in user , Consumed the service propagating this token , and read the data .
The frontend user has both the roles assigned to him.
Its an inter app communication scenario , propagating the users' scope between them . Just like a trusted RFC call in ABAP perspective.
detailed project uploaded to git
https://github.com/sreehari-pillai-atom/cf-interapp-conn/tree/main
Sreehari
One comment I have about token exchange:
Usually, what you want to achieve:
User uses Orderapp and has the required rolecollection for orderapp, which is user-facing.
User does not need to know about backend-scopes, like masterdata, as these are not user-facing data.
It is orderapp-internal knowledge, that orderapp calls some other external services.
User should not be bothered with that.
THEREFORE, the tokenexchange was invented, I believe.
The user has orderrole - and orderapp does tokenexchange in order to get the masterdata scope (not role)
Masterdata is not user-facing, so there are no role-templates there
This is what I would expect as normal use case.
Of course, application setup can be different and users might need to know about both apps, why not. I mean, a user-facing app can call a second user-facing app.
However, I would suggest to carefully overthink the design in that case.
E.g. it might be better to have a third module, head-less service, which is used by both user-facing apps. As such, avoiding that users need both roles. Always better to have focussed narrow role-assignment.
Just my 2 cents
Excellent, thanks for this - I was mixing up the requirement of needing to have one app containing scope references of other apps .
always nice talking to you 😉