Skip to Content
Technical Articles
Author's profile photo Carlos Roggan

Cloud Integration: How to call iFlow from XSUAA-based application in BTP (Inbound)

SAP Cloud Integration (aka CPI) allows to call an integration flow via HTTP request from an external system (HTTP Sender adapter).
In this tutorial we create an app in Cloud Foundry that is protected with an own instance of XSUAA  and calls an iFlow.
Since the iFlow is protected via different XSUAA and requires different user role, we need a special blog (see below) post to learn how this scenario can be realized.
If your app is protected with IAS, try this blog post.
Technologies covered:
SAP Business Technology Platform (BTP), Cloud Foundry
SAP Cloud Integration (CPI)
XSUAA, OAuth 2.0

Quick Guide
Sample Code


0.1. Prerequisites
0.2. Preparation
0.3. Introduction
1. Create iFlow and Role
2. Create Frontend Application
3. Assign Roles
4. Run
Appendix: Sample Application Code

0.1. Prerequisites

  • Access to BTP and admin permissions in subaccount
  • Access to a Cloud Integration tenant.
  • Familiar with Node.js, although the tutorial can be followed without local Node.js installation.

0.2. Preparation

In the course of the tutorial we’re going to create a node app, so we need a project folder on file system.
In my example, the structure looks as follows:

The full file content can be found in the appendix.

0.3. Introduction

We have a scenario where an application – deployed to BTP – is supposed to call an iFlow endpoint.
An iFlow can expose a REST endpoint by adding an HTTPS Sender adapter.
As such, our app only needs to fire a REST request.
Things usually tend to get complicated when we add security.
So don’t add
Adding security is not negotiable, so we can stop discussing this point.
Instead, it is preferrable reading through this tutorial.
For what?
If myself is able to learn it -> anybody can do.

Have I mentioned that endpoints have to be protected with OAuth instead of Basic Auth, because it is more secure?
More secure means much more complex – but again, I was able, so anybody can understand….
What is OAuth?
According to the OAuth concept, the client registers at Authorization Server and gets an ID and a password (referred to as clientid/secret in the binding or in the service key of an instance).
So the client is allowed to contact the Auth Server (XSUAA) and gets a token, which can be used to access the protected resource (iFlow and node app).
The protected resource validates the incoming token by checking if it was issued for the expected client (among other checks).

In our scenario we have a user-centric application (Frontend App) which obviously uses an instance of XSUAA to protect its endpoint.
The credentials of this XSUAA instance contain the client ID, let’s call it “frontXsuaa”.
Our application uses the Approuter to handle the user-login.
Login is done via the OAuth flow called Authorization Code which is super secure.
Approuter is bound to the same instance of XSUAA.
On user-login, the Auth Server (which is the XSUAA server available for the subaccount) issues a JWT token which is forwarded to our node app.
The app validates the token.

What is a JWT token?
The token is a small piece of data, it is a JSON-formatted String which comes Base64-encoded.
It can be decoded easily (yes, we’ll do so ourselves) and the content can be viewed:
The content contains lot of information, e.g. the ClientID for which it was issued.
In our example: “frontXsuaa”.
So the app can check whether it is the same client ID like the one which the app finds in the binding.
This info can be found in a property called aud which stands for audience: the token was issued for this audience, for this client.

In our example:
App is bound to XSUAA instance with name “frontXsuaa”
User logs in to our app
a token is issued
token contains the info: aud=”frontXsuaa”
App looks into XSUAA-binding which contains credentials
App finds clientid=”frontXsuaa”
So: go ahead

That’s great
However, there’s a bad news.
The drawback:
Our protected frontend application
cannot use this JWT token
for calling an iFlow endpoint
which is protected
with a different XSUAA instance.

The iFlow endpoint is protected with OAuth and uses XSUAA for it.
The credentials for this XSUAA instance (let’s call it “iflowXsuaa”) can be found in service key of a it-rt service instance.
This is a different OAuth client with a different ID.
When the frontend app calls the iFlow, the existing frontend-token (aud = “frontXsuaa”) is validated by CPI runtime which expects aud = “iflowXsuaa”.
Therefore rejected.

Below diagram tries to visualize my security-bla-bla:

And the next diagram shows the same with some details.
No further bla required:

After bla-bla-ing the challenge, let’s mention the solution.
Ehhmm, before mentioning OUR solution, let’s give a little text (bla) about a POSSIBLE solution:

Whenever we want to call an xsuaa-protected endpoint, we can use the simple client-credentials flow.
(if we have the credentials, in binding or service key).
Client-credentials is a simplified OAuth-flow which doesn’t require a user.
In our case, the node app is bound to the instance of it-rt (CPI runtime service instance), so it could read the cpi-credentials from the binding.
Using these credentials, it could call XSUAA and get a token which is accepted by CPI.
This would work.

Then…?  Google (Noto Color Emoji - Unicode 15.0)
The BETTER solution for our scenario is the OAuth flow called jwt-bearer.
It is similar, but adds user-information to the fetched token.
How is it done?
In addition to the cpi credentials, we send the existing JWT token to the Auth Server.
The result is a new token which is like a mixture of the best characteristics of both tokens:
1) First token
The first token is a result of a user-login.
It contains the frontend-clientid, audience, etc, for the frontend-app.
And it contains info about the user, like name, email, user-roles, favorite pets. 😺
2) Second token
It contains the cpi-clientid, audience.
It is issued for CPI, which is what we need.
Now, due to jwt-bearer, additionally it contains the user info, mixed in from the first token.
BTW, this process is also called Token Exchange: we send a token and get a new token.

It is not a sole exchange of tokens, because we also have to send the clientid/secret for the target (cpi).
See my blog series to learn more about Token Exchange.

Below diagram shows the 3 steps:

First step:
The user logs in to the frontend app and a JWT token is issued for the app by fontend-XSUAA.
Second step:The app fetches a token for CPI via jwt-bearer flow, from cpi-XSUAA.
Third step:
The new JWT token is used to call the iFlow endpoint.

Enough bla?
Unfortunately, up to now we’ve only been talking about authentication.
Which was successful
However, to be more secure, the (already secured) endpoints typically require that the calling user has a certain role.
This aspect is called authorization.
It is more safe, because the app can decide what an authenticated user is allowed to do.
E.g. only a user who has the “create” role is allowed to create anything.
Can we skip this?
We have to cover this security aspect, because the iFlow adapter configuration forces us to protect the endpoint with a role.
And our node app wants to be safe as well, so it uses a role as well.
A different role, it is not possible to re-use the iFlow-role.
As such, our end-user has to wake up the admin and ask to assign our app-role to him, for accessing the app.
OK, happy.
However, when doing the token exchange, the resulting JWT token is fine…… but it doesn’t contain the role required by iFlow.
The solution:
The admin has to wake up again and assign the iFlow-role to the user as well.
Then it is fine: after token exchange, the cpi-token contains the required role.

I’d like to mention that this is a simple procedure, but in my eyes, it is a basic learning:
🔸 The scenario is such that the end-user uses only the frontend app.
He doesn’t know that the app internally calls the iFlow to get some data from some connected backend.
As such, I would expect that the 2 apps (frontend + iFlow) are configured in a way which results in the JWT token getting the cpi-role. With grant. as explained in this blog post.
🔸However, in our scenario, this configuration (grant-statement) is not possible, as we don’t have access to configure the (internal) XSUAA of CPI.
🔸Therefore, our procedure is like a useful workaround:
The user gets a role (cpi) which he doesn’t seem to require.
After login, the JWT token contains only the scope defined by the app.
Nevertheless, the token also contains information about assigned user-roles
This info is crucial when doing token-exchange.
As a consequence, during exchange, XSUAA adds the cpi-scope to the new token.

Below diagram tries to show the required authorization artifacts and their relation:

A token for the iFlow can be fetched using the cpi-credentials ➕ the frontend-user-token (jwt-bearer).This procedure has the advantage of preserving user information in the token.
To get the user-role, which is required by iFlow, into the token, the user gets this role assigned via role collection in BTP


Finished bla for now, so let’s get started with the hands-on.

1. Create iFlow and Role

The first short part of the tutorial takes place in the CPI tenant.
We create a simple iFlow that can be called by our node app.
The iFlow should be protected by a user role .

1.1. Create User Roles

First of all, we create a user role in CPI -> Monitor Artifacts -> Manage Security -> User Roles
We “Add” a role with name:

Optional: View Roles
After creating User Roles in the CPI dashboard, the standard role mechanism of BTP is triggered under the hood. Means that scopes and role templates and default roles are generated under the hood.
See: Subaccount -> Security -> Roles -> filter for Ntif

I recommend this little chapter for understanding Roles in BTP.

1.2. Create iFlow

Our integration flow is very simple:
We only need to create an HTTPS sender adapter.
No more steps are required.
When we call the endpoint from our app, we’ll receive a response from CPI anyways.

The configuration of the HTTP adapter:

🔸 Address
set to /cpitex
Note that this endpoint name must match the endpoint used in our node app
🔸 Authorization
Set to “User Role”
🔸 User Role
We press the “Select” button and choose “cpitex.Read”
Checkbox disabled.

After configuring the HTTPS adapter, we can save and deploy the iFlow.

2. Create Frontend Application

We want to create a very simple frontend application that is protected with OAuth via XSUAA.
In addition, access to the app endpoint is controlled with a user role.
The login is handled by Approuter.
The app does nothing than calling the iFlow endpoint.
However, there are 2 challenges:
The iFlow is protected with a different instance of XSUAA, so we cannot just forward the JWT token.
The iFlow requires a user role, so we need to get the required scope into the token.

First we create 2 required service instances.

2.1. Create XSUAA Service Instance

We create the instance of xsuaa which is used to protect our frontend app.
In the config file, we define the scope and the role which we require:

🔷 xs-security.json

    "xsappname": "cpitexxsappname",
    "scopes": [{
            "name": "$XSAPPNAME.texfrontendscope"
    "role-templates": [{
            "name": "TexFrontendUserRole",
            "scope-references": ["$XSAPPNAME.texfrontendscope"]

This name must be unique and it is the base for the client ID
The technical fine-granular base for user role
At runtime, after service creation, a user role is created based on this template.
Creation can be done manually in the cockpit, if the template contains dynamic attributes.
Otherwise, like in our example, a default role is created automatically.
The role is like a wrapper around one or more scopes.

To create the service instance, we run the following command from our project directory:
cf cs xsuaa application cpitexXsuaa -c xs-security.json

Optional: View Role
After instance creation we can go to the cockpit and have a look at Subaccount -> Security -> Roles
There we can see that an “Application” with name “cpitexxsappname” is listed with the role template which we defined in our xs-security.json file.
In addition, we can see that a default role with same name has been generated under the hood.
The “Create Role” button is disabled, because one role is already existing. Our role is not customizable, so no duplicates can be created.

2.2. Create CPI Service Instance

We need to create an instance of “Process Integration Runtime” because it provides us with access to the iFlow endpoint.
Creating an instance of cpi-runtime will lead to creation of an instance of XSUAA under the hood.
The hidden XSUAA instance is configured with the settings that we provide for the cpi-runtime.
These are the settings:

🔷 config-cpi.json


This is a kind of filter.
At runtime, the iFlow will be called with a jwt token, and the jwt token will contain a property (claim) which carries the information about how it was obtained.
In our example, if the jwt token would have been fetched by using e.g. the client-credentials flow, then the access to the iFlow would be denied.
We allow only tokens that are fetched with token exchange (jwt-bearer).
The roles specified here will be added to the JWT token.
However, the roles must have been created beforehand in the CPI dashboard.

To create the instance:
cf cs it-rt integration-flow cpitexIflow -c config-cpi.json

2.3. Create Application

Our application consists of 2 modules: Approuter and the application itself

2.3.1.  Approuter

Approuter is typically used by people-centric applications. Users access the app via Approuter, which handles the login.
Approuter is an existing node module provided by SAP.
It only needs to be configured.
During deployment, it is downloaded and started as configured in its package.json file.

🔷 package.json:

    "dependencies": {
        "@sap/approuter": "latest"
    "scripts": {
        "start": "node node_modules/@sap/approuter/approuter.js"

So when a user accesses our application, he will open the URL of Approuter.
Our task is to define a route from Approuter to our app:

🔷 xs-app.json:

  "authenticationMethod": "route",
  "routes": [{
      "source": "^/routemeto/(.*)$",
      "target": "$1",
      "destination": "destination_cpitex",
      "authenticationType": "xsuaa"

The snippet shows:
Approuter will handle the login with IAS.
The user will type an entry url following this scheme:
The serviceEndpoint is looked up at the destination which is created in the cockpit or simply defined in our manifest (favorite approach for POCs and tutorials).
Personally, I like to know what I’m doing, so I like to use silly names and explicit statements, even though I don’t recommend it for productive code, for obvious reasons.
So I’m adding a route segment with silly name “routemeto”.
This name is chosen to make clear that we’re invoking a “route”, not a real endpoint-URL

2.3.2. Application

Our application has only one job to do: call the iFlow endpoint.
As a result, it just displays some interesting information about JWT-internals on the browser.
Our app is accessed via a REST endpoint.

First of all, we check if the user who is accessing our app, has the required user role assigned.
The check is performed with the help of a convenience method, which internally validates the incoming JWT token.
We have to copy the scope name which we defined in the xs-security.json file.

isScopeAvailable = req.authInfo.checkScope(CREDENTIALS.xsappname + '.texfrontendscope')
if (! isScopeAvailable) {
    res.status(403).end('Forbidden. Missing authorization.') 

A similar check is performed by CPI under the hood, when we call the iFlow endpoint.

Coming to the interesting part:
We grab the JWT token, which was issued during user-login, from the request.
This token is used for token exchange:

userToken = req.tokenInfo.getTokenValue()   
cpiToken = await _doTokenExchange(userToken)   

The token exchange is a “normal” HTTP request to the authorization server (XSUAA) to fetch a JWT token, which can be used to call the iFlow endpoint:

async function  _doTokenExchange(jwt) {
    const grant = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
    const options = {
        url: CREDENTIALS_CPI.tokenurl,
        headers: {
            Authorization: "Basic " + Buffer.from(CREDENTIALS_CPI.clientid + ':' + CREDENTIALS_CPI.clientsecret).toString("base64"),
            'Content-Type': 'application/x-www-form-urlencoded'
        method: 'POST',
        data : `grant_type=${grant}&response_type=token&assertion=${jwt}`
    const response = await axios(options)

To fetch a token for CPI, we need the CPI-credentials, which we have in the binding of our app.
These are the clientid and clientsecret of the XSUAA instance which is used to protect the iFlow
In addition, we send the existing user-JWT in the request body.
This OAuth flow is called “JWT Bearer” and is defined in RFC 7523.
In the context of Cloud Foundry and XSUAA, the CF UAA reference is useful.

Below snippet shows how we get the credentials for fetching a CPI-JWT from the binding:

INSTANCES = xsenv.getServices({
    myCpi:  {name: 'cpitexIflow'}

Note that here we’re passing the name of the instance of the CPI Service.
In case you’re using a different name, make sure to adapt it here.

The result of the token exchange is a new JWT token that can be used to call the iFlow endpoint.
it also contains information about the user that logged in to our frontend application.
The iFlow can use this user information in order to propagate the identity to a connected backend (principal propagation).

This is how we call the iFlow:

async function _callCPI(jwt) {
    const options = {
        url: `${CREDENTIALS_CPI.url}/http/cpitex`,
        headers: {
           'Content-Type': 'application/text',
           'Authorization': 'Bearer ' + jwt
        method: 'POST',
        data : "Sending some Data from node application in BTP Cloud to iFlow endpoint"

    const response = await axios(options)

The name of the endpoint must match the configuration of the HTTPS Sender adapter.

This call is only successful if the exchanged token contains the scope required by the iFlow.
The scope will be contained only if the user has the CPI-User-Role assigned in a Role Collection.
The CPI-User-Role, which has to be assigned, is configured in the HTTPS Sender adapter (see chapter 1.1. above).
The response will contain the same text that we send in the POST request, because our iflow does nothing with it, so it is just returned.
The rest of your frontend app sample code is just doing some formatting of the 2 JWT tokens, such that we can see the info in the browser.

The full code can be found in the appendix.

2.4. Deploy

The deployment descriptor, the manifest.yml, doesn’t contain any surprising configurations (see appendix), so we can go ahead and deploy our application:
cf push

3. Assign Roles

We’ve finished developing our iFlow and our frontend app, but before we can run the scenario, we need to do little configuration in the cockpit.

Just a few standard clicks, but from my point of view, it is essential for understanding the concept:
The user who should be authorized to access our frontend application needs to have the privileges for the frontend app.
BUT he needs to have the privileges of the iFlow as well. Even though he doesn’t know that our app calls the iFlow internally.
As such, we have to assign both the frontend-app role and the cpi role to our user.

3.1. Create Role Collection

In BTP, Roles are assigned via “Role Collections”.
We go to
Subaccount -> Security -> Role Collections  -> Press big blue ➕
We create a Role Collection with a name of our choice, e.g. “CPI_TEX_RC”
We open the role collection and press the “Edit” Button.

3.2. Assign Frontend Role

In the role selection dialog, we search for the role name “TexFrontendUserRole”
And we add it to the role collection.

3.3. Assign CPI Role

Similarly, we search for “Cpitex.Read” and add the role to the collection.

3.4. Assign user

Finally, we add our BTP user to the collection.

4. Run Scenario

To run our scenario, we invoke our app via the following url (in my example):

After login, we get the following result:

We can nicely see the differences between the 2 tokens:

Looking at the aud claim:
The first token was intended only for the “cpitexxsappname” client, defined in the xs-security.json file of frontend app.
This means that the token cannot be sent to the iFlow endpoint which has a different clientid (it is a guid, see below).

Looking at the scope claim:
We can see that the first token contains only the scope of the frontend app.
So the user has 2 roles, frontend and CPI-Role, but the token for cpitexxsappname client gets only one scope.
Which is correct, of course.

Looking at xs.system.attributes:
We can see that the information about assigned role collections is available in the tokens.
This is what makes it possible to obtain the “cpitex.Read” scope in the second token.
Also, we can see that user information like “name” is available in the CPI-token.
It is just an excerpt, other claims like email are contained as well.

5. Clean Up

To help you with housekeeping, please find below the required steps for your convenience.

🔹 Delete app and approuter
cf d -r -f cpitex
cf d -r -f cpitexrouter
🔹 Delete service instances
cf ds -f cpitexXsuaa
cf ds -f cpitexIflow
🔹Delete Role Configs
Delete Role Collection
Delete User Role in CPI (this deletes also the role in BTP)
Application role is deleted during service deletion
🔹Delete iFlow


In this blog post we’ve learned how we can call an iFlow that is protected with User Role, from an application that uses a different instance of XSUAA to protect itself.
It is possible via token exchange.
Need to know:
We must assign the CPI-User-Role to the users of our application.

What else?

See this blog to learn how to call an iFlow from an application that uses IAS to protect itself:


SAP Help Portal
Creating Service instance and Key for Inbound Authentication
Examples for Service Instance and Key Parameters.
Reference for xs-security.json file in the SAP Help portal.

npm site for xssec library.
SAP Approuter

Blog Posts
Mandys blog post about inbound connection.
My blog post about XSUAA user attributes.
Understanding Token Exchange
Tutorial for using destination configuration with token exchange type.
Tutorial for granting scopes.
OAuth for dummies, explained by Dummy.
Info about the content of JWT tokens, explained in my dummy way.
Introduction and first dummy steps with approuter.
Blog about user roles and attributes and accessing JWT in script
Security Glossary Blog

JWT specification: rfc7519
IANA JWT Claims.
Spec for “JWT Bearer”: RFC 7523.
Cloud Foundry UAA docu: introspect token
Cloud Foundry CLI Installation guide.
Cloud Foundry UAA reference for jwt-bearer
Spec for token exchange, i.e. request access token via JWT bearer token
OAuth 2.0 Token Exchange rfc8693

Appendix 1: Sample Application Code

Project Structure:

You might need to adapt the app names in manifest and the domain of the routes.
Also, if you changed the name of the target endpoint, make sure to adapt.





    "xsappname": "cpitexxsappname",
    "tenant-mode": "dedicated",
    "scopes": [{
            "name": "$XSAPPNAME.texfrontendscope"
    "role-templates": [{
            "name": "TexFrontendUserRole",
            "description": "Role for end users, allows to login to TEX app",
            "scope-references": ["$XSAPPNAME.texfrontendscope"]


  - name: cpitex
    path: app
    memory: 64M
      - nodejs_buildpack
    - route:
      - cpitexXsuaa
      - cpitexIflow
  - name: ntiftexrouter
      - nodejs_buildpack  
    - route:
    path: approuter
    memory: 128M
      destinations: >
            "forwardAuthToken": true
      - cpitexXsuaa



  "dependencies": {
    "@sap/xsenv": "latest",
    "@sap/xssec": "^3.2.13",
    "express": "^4.17.1",
    "axios": "0.24.0",
    "passport": "^0.4.0"


const xsenv = require('@sap/xsenv')

const INSTANCES = xsenv.getServices({
    myXsuaa: {tag: 'xsuaa'},
    myCpi:  {name: 'cpitexIflow'}

const axios = require('axios')
const xssec = require('@sap/xssec')
const passport = require('passport')
const JWTStrategy = xssec.JWTStrategy
passport.use('JWT', new JWTStrategy(CREDENTIALS_UAA))
const express = require('express')
const app = express();
app.use(passport.authenticate('JWT', { session: false, failWithError: true }))

// start server

app.get('/homepage', async (req, res) => {

    // check if user has required role    
    const isScopeAvailable = req.authInfo.checkScope(CREDENTIALS_UAA.xsappname + '.texfrontendscope')
    if (! isScopeAvailable) {
        res.status(403).end('Forbidden. Missing authorization.') 
    // exchange app-user-token for CPI-token
    const userToken = req.tokenInfo.getTokenValue()   
    const cpiToken = await _doTokenExchange(userToken)   
    // call iFlow
    const cpiResult = await _callCPI(cpiToken)

    // print token info to browser
    const htmlUser = _formatClaims(userToken) 
    const htmlExchanged = _formatClaims(cpiToken) 

    res.send(`  <h4>JWT after user login</h4>${htmlUser}
                <h4>JWT after token exchange</h4>${htmlExchanged}
                <h4>Response from iFlow</h4>${cpiResult}.</p>`)

/* HELPER */

async function  _doTokenExchange(jwt) {
    const grant = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
    const options = {
        url: CREDENTIALS_CPI.tokenurl,
        headers: {
            Authorization: "Basic " + Buffer.from(CREDENTIALS_CPI.clientid + ':' + CREDENTIALS_CPI.clientsecret).toString("base64"),
            'Content-Type': 'application/x-www-form-urlencoded'
        method: 'POST',
        data : `grant_type=${grant}&response_type=token&assertion=${jwt}`
    const response = await axios(options)

async function _callCPI(jwt) {
    const options = {
        url: `${CREDENTIALS_CPI.url}/http/cpitex`,
        headers: {
           'Content-Type': 'application/text',
           'Authorization': 'Bearer ' + jwt
        method: 'POST',
        data : "Sending some Data from node application in BTP Cloud to iFlow endpoint"

    const response = await axios(options)

function _formatClaims(jwtEncoded){
    const jwtDecodedJson = new xssec.TokenInfo(jwtEncoded).getPayload()
    const claims = new Array()
     claims.push(`issuer: ${jwtDecodedJson.iss}`)
     claims.push(`<br>client_id: ${jwtDecodedJson.client_id}</br>`)
     claims.push(`grant_type: ${jwtDecodedJson.grant_type}`)
     claims.push(`<br>scopes: ${jwtDecodedJson.scope}</br>`)
     claims.push(`ext_attr: ${JSON.stringify(jwtDecodedJson.ext_attr)}`)
     claims.push(`<br>aud: ${jwtDecodedJson.aud}</br>`)  
     claims.push(`origin: ${jwtDecodedJson.origin}`)
     claims.push(`<br>name: ${jwtDecodedJson.given_name}</br>`)
     claims.push(`xs.system.attributes: ${JSON.stringify(jwtDecodedJson['xs.system.attributes'])}`)

     return claims.join('')



    "dependencies": {
        "@sap/approuter": "latest"
    "scripts": {
        "start": "node node_modules/@sap/approuter/approuter.js"


  "authenticationMethod": "route",
  "routes": [
      "source": "^/routemeto/(.*)$",
      "target": "$1",
      "destination": "destination_cpitex",
      "authenticationType": "xsuaa"

Assigned Tags

      Be the first to leave a comment
      You must be Logged on to comment or reply to a post.