Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
CarlosRoggan
Product and Topic Expert
Product and Topic Expert
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

Update:
The reference for the cross-subaccount grant has been removed from the documentation.
Hence the link provided below doesn't exist anymore.
Reason: it is not recommended to establish grants that exceed the limit of subaccounts, as stated by the product owner.

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

Security Glossary.

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


 
17 Comments