Skip to Content
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, xxx)”]

The consuming app accepts it by pointing to the xsuaa instance of the providing app

“authorities”:[
“$XSAPPNAME(application,xxx).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

 

11 Comments
You must be Logged on to comment or reply to a post.
      • 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  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 ?

    app.use(jwtLogger)
    app.use(passport.initialize());
    app.use(passport.authenticate('JWT', { session: false }));

    and also, my provide app can be successfully consumed by jobscheculer by using.

    "grant-as-authority-to-apps": [
            "$XSSERVICENAME(testauto_job_sched)"
          ]

     

    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.

    {
      "xsappname": "service-multi-tenant",
      "tenant-mode": "shared",
      ...
      "authorities": ["$XSAPPNAME.Callback","$ACCEPT_GRANTED_AUTHORITIES"],
    ...
    }
          

    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.

    {
      "xsappname": "iot-clnt-1-cf",
      "tenant-mode": "dedicated",
        "scopes": [{
    		"name": "$XSAPPNAME.service-multi-tenant",
    		"grant-as-authority-to-apps" : [ "$XSAPPNAME(application,<subaccount_ID>, service-multi-tenant)"]
      }]
      "attributes": [],
      "role-templates": []
    }
    

     

    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.

      • Not sure if you scenario is client-credentials? If you’re using a UI5 app,with approuter, then the issued JWT token won’t have the client-credentials tag. So you would need to use the “granted-apps” statement (as described in the other blog)
      • One more point: When granting a scope to a different xsappname, then you have to reference the xsappname. You’re doing that. But in the statement you have to give the <service-plan>, which is usually “application”. But in your scenario I think it should be “shared”, right?
      • On the other hand, I don’t know if it can be mixed, but would be an interesting info, if you find out
      • On the other hand, I’ve had a look at the docu, and it says that “currently only application is supported” See here and chapter “Referencing the application”
        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