Technical Articles
How to call protected app | from external app || from different subaccount
This blog post is an addendum to the previous blog post
How to call protected app | from external app || as external user ||| with scope
Two Node.js apps deployed to SAP Cloud Platform, Cloud Foundry Environment
Quicklinks:
Quick Guide
Sample Code
Why do we need an addendum?
Because the previous scenario works only if both apps and bother xsuaa instances are located in the same subaccount
Recap:
In the previous tutorial we learned how an app can assign a scope to a second app.
The providing (xsuaa-protected) app requires a scope and grants it to the second app
This is done by pointing to the xsuaa instance of the consuming app
“grant-as-authority-to-apps” : [
“$XSAPPNAME(application, yyy)”]
The consuming app accepts it by pointing to the xsuaa instance of the providing app
“authorities”:[
“$XSAPPNAME(application,yyy).scopex”]
As we can see, the xsuaa-instances (oauth-clients) identify each other by the name
The name is the value of the property xsappname, as specified in xs-security.json file
Problem:
One xsuaa has to grant the scope to the other xsuaa (roughly speaking)
oth know each other because they’re located in the same identity zone
But what if both xsuaa instances are located in different subaccounts (indentity zones?
Solution:
The granting xsuaa has to identify the target subaccount
So we have to point to it in the grant statement
“grant-as-authority-to-apps” : [
“$XSAPPNAME(application, 123-id, name)”]
The consuming xsuaa just accepts all granted scopes
“authorities”:[
“$ACCEPT_GRANTED_AUTHORITIES”]
Constraint:
This mechanism works only inside one data center
More insights:
More information about JWT tokens for newbies can be found in this blog post
Hands-On
To get this scenario running, we need the following prerequisites
Prerequisites
- the prerequisites of the previous blog
- in addition, we need a second subaccount.
This can be any trial account (with any user) as long as it is located in the same data center
Overview
1 Create API Provider App
2 Nothing for today
3 Create Client App and call API
Appendix: All Sample Project Files
Preparation: Create Project Structure
Same as previous preparation
Note:
If you’ve followed the previous tutorial, you can just reuse everything.
However, make sure to delete and re-create th xsuaa instances from scratch
Furthermore, you should undeploy the providerapp, otherwise there will be a name clash
If you decide to deploy the providerapp with different name, then you need to adapt the target URL in the client app
This is the project structure for our tutorial
Step 1: Create API Provider App
Almost everything is the same as in previous tutorial.
1.1. Create XSUAA instance
The only difference is the xsuaa instance: it contains the modified grant statement
We have to find the ID of the subaccount of the client app
It is easy to find, we can see it in the cloud cockpit, in the overview section of the subaccount
Then we add it to the grant statement, as second parameter
The syntax:
“grant-as-authority-to-apps” : [
“$XSAPPNAME(<service_plan>,
<subaccount_id_of_caller>,
<xsappname_of_caller>
]
The content:
{
"xsappname" : "xsappforproviderapp",
"tenant-mode" : "dedicated",
"scopes": [{
"name": "$XSAPPNAME.scopeforproviderapp",
"granted-apps" : [ "$XSAPPNAME(application,xsappforhumanuser)"],
"grant-as-authority-to-apps" : [ "$XSAPPNAME(application, 123-123, xsappforclientapp)"]
}]
}
Note:
In today’s tutorial, we’re skipping the test with human user, so we don’t need to define a role
Note:
The second difference is the target account
Before creating the instance of xsuaa, we have to make sure that our CF CLI points to the correct subaccount
We can check it: cf target
Or cf t
And we can change it. First get orgs: cf orgs
Then change: cf t -o 123456trial
Finally create the service instance: in folder apiproviderapp, run the following command
cf cs xsuaa application xsuaaforprovider -c xs-security.json
1.2. Create app
Everything is described in the corresponding section of previous tutorial
1.3. Deploy
Again, nothing new here, we just have to make sure that we deploy to the correct account.
In my example, it is the Trial account.
Anyways, it is not the account with ID 123-123
Step: 2 Call API with human user
We skip this step today
Step 3: Create Client App and call API
Again, everything is the same as in the corresponding section of the previous tutorial
Only the security descriptor is slightly different
3.1. Create xsuaa for client app
I didn’t find a way how to add the ID of the granting subaccount, to make the accept statement concrete
So we have to use the generic statement, to simply accept all
So the xs-security.json file has to be as follows:
{
"xsappname" : "xsappforclientapp",
"tenant-mode" : "dedicated",
"authorities":["$ACCEPT_GRANTED_AUTHORITIES"]
}
Before we create the service instance, we have to make sure that we’re targeting the desired account of SAP Cloud Platform.
I don’t know which account you have to target, but in any case, it is not the same like in Step 1
So we need
cf t -o <other_org>
If you’ve followed the previous tutorial, I recommend you delete the existing service instance, to avoid trouble
Afterwards, to create this service instance, we have to make sure to step into ithe clientapp folder
Then
cf cs xsuaa application xsuaaforclient -c xs-security.json
3.3. Create the client app
There’s no change in the code of the client app compared to the previous tutorial
See appendix for full file content
3.4. Deploy the client app
There’s no change to the deployment process (obviously)
Also, calling the URL is the same
https://clientapp.cfapps.eu10.hana.ondemand.com/trigger
And the result is the same:
This (boring) success message proves:
The client app has sent a JWT token which contains the required scope
That scope was defined by the provider app and it is really required by the provider app (it is checked), otherwise the call would fail
Recap
To enable communication between 2 apps, using different xsuaa, in different subaccount:
1. protected app: “grant” the scope to the client-xsuaa with subaccount ID
2. client app: the “authorities”to accept all granted
Diagram
Summary
In this blog post, we’ve learned how to realize client-credentials scenario across subaccount borders
We’ve successfully tested it by calling an app in trial account from productive account
The interesting part was how to grant the scope, without user interaction
Troublemaking
See here
In addition, I’d like to repeat that you might get trouble if you don’t delete the existing instance of xsuaa, if remaining from previous blog post. The oauth client needs a new grant, the scope has to be newly granted otherwise the generated XSAPPNAME is different, and the JWT token rejected
Quick Guide
The protected app has to grant the scope to the consumer
If both are not located in the same subaccount, then the subaccountID of the consumer has to be added (ID can be found in the cockpit)
"scopes": [{
"grant-as-authority-to-apps" : [
"$XSAPPNAME(application, 12344-abc, xsappforclientapp)"
The calling client has to accept the grant. In case of different subaccounts, the generic statement has to be used to accept all granted scopes
"authorities":["$ACCEPT_GRANTED_AUTHORITIES"]
Links
Docu: SAP Help Portal
Useful info about security in this series: Blog series
JWT tokens info in this blog post
OAuth: here
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, 123-abc, xsappforclientapp)"]
}]
}
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
Awesome Blog!, thanks for sharing.
Hi Prakritidev Verma , thanks to YOU for the nice feedback ! 😉
I am developing a multi tenant application in node.js wiith express. I'm confused how to use XUAA with passport for security, as all the examples are using CDS and I'm not using cds.
I have this functionality of /signup and /login in my back end. I'm using Hana database for storing the user credentials, but I'm not fully able to understand how to use XUAA with passport and do the password matching with credentials stored in my database.
I used to work on MongoDB so this is a little bit confusing for me.
It would be great if you have anything that can help me on this, or if you wrote any blog on this.
Thanks.
Hi, maybe we can try to figure out via chat, you can contact me. Cheers, Carlos
Oh Great!
Sure thanks.
Hi Carlos Roggan
I followed you steps in my applications(cross subaccount),my applications are developed by java in CAP.
in my case, the token used in my provider app is the same token from client app(by write log), then authorization check failed, because the client app token does not contains the required scopes for provider token.
I also read you blog https://blogs.sap.com/2020/09/03/outdated-sap_jwt_trust_acl/
I’m not sure when and how does the providerapp exchange the clientapp token(provideuaa grant scops to clientuaa, the red line on the 5th figure of this blog), then use the new token. is it handled by cloud foundry platform or by providerapp it self?
I see the code from you sample, does it use to exchange the token ?
and also, my provide app can be successfully consumed by jobscheculer by using.
do you have any suggestion?
Regards
Eric
I figured it out. it needs the client credential, the oauth client credential has the provider app granted scopes. I passed the user credential, it does not have...
thanks for your so many details .
Regeards,
Eric
Hi Eric, thanks for the feedback and thanks for the clarification comment. This will help others.
I've also added the info to the troublemaking section
Cheers,
Carlos
Hi Carlos,
Thanks a lot for all your blogs. Im a big fan.
We have a somewhat complex scenario. ill try to explain it:
We have a multi-tenant application hosted in subaccount A (the provider account). This application has (among other things) a fiori lauchpad with a few apps.
The multi tenant subaccount has a uaa service with the tenant-mode set to shared.
Then we have another subaccount B (client) who is subscribed to the service provided by subaccount A.
Besides that each client can have their own versions of specific apps that are called by the apps in the multi-tenant fiori lauchpad. The apps (as they run in a subaccount B) have their own uaa.
We use destinations in the subaccounts to access the client specific apps running in the subaccounts.
!!! THE PROBLEM !!!
If i understood the blog correctly then in this case our client would be the First subaccount (since the fiori app is hosted there) and our provider should be the secound subaccount where the client specific app is we want to call.
Every time we perform the call we get a 401 back.
im not really sure what we are doing wrong my question is. Does this scenario work with mult-tenant apps?
If it does do you have any idea as to what we are doing wrong?
Kind regards
David Sooter
Hello David Sooter ,
Thanks a lot for the nice feedback !
About your question, as far as I understand, the files look quite good. I haven't tried such scenario, so I cannot give good advice.
However, here are a few points, I would suggest to look at.
It says: "Note:Currently, you can only use the application service plan."
Maybe you can try your scenario with "application" service plan and check if it works, then maybe youl would have to workaround your scenario with a second xsuaa-instance?
Hope this helps,
Cheers,
Carlos
Hello, Carlos Roggan,
We did manage to get it to run, but when we now try to access the app via a destination, we get stuck with a endless loop in our browser. Did you ever encounter this behaviour and if so, could you point us in the right direction ?
Regards,
Florian
Hello,
It is possibile to forward the token from an ui5 application to a cds where ui5 application is located in one subaccount and cds in another ?
I think that in this case I should use user credential scenario because I should forward the jwt token of the logged user. I have tried with granted app but on the cds I will not receive the scope that I have defined in the cds but only scopes from ui5 application
it seems that only client credential case is working cross subaccount 🙁
Hello, Carlos Roggan,
I have a scenario , where we have 2 micro reusable services granting access to the main reusable service xsuaa.
This main service is consumer by consumer by subscribing to the instance created on top of the main service exposed as reusable through service broker .
After successful subscription in the consumer and binding it to the approuter(approuter has it's applicationplan based xsuaa) , I am getting 401 error if I try to consumer the any of the 2 small micro service .
Have you come across any such scenarios .
Any inputs would be appreciated
Hello Carlos Roggan
thank you for this blog post.
I checked the reference documentation here
Application Security Descriptor Configuration Syntax | SAP Help Portal
and I think it is missing the $XSAPPNAME with three parameters (including subaccount) that you use here. Since we are using this scenario I wonder: Is this just missing in the documentation and should be added or is this not "offically" supported?
Also in this documentation
Technical Communication with Tightly Coupled Developments | SAP Help Portal
it is only assuming the same subaccount.
Am I missing something or is the documentation just incomplete?
Here is the answer for the lack of documentation: "We don't recommend to use three parameters. For this reason, you won't find it in the official documentation. We are talking here about grants that exceed subaccount limit, and we do not want to implement this kind of grants. (product owner's statement)"
https://github.com/SAP-docs/btp-cloud-platform/issues/195
So I guess it is better to not rely on this behavior too much.