SAP Cloud Integration (aka CPI) allows to call an integration flow via HTTP request from an external system (HTTP Sender adapter).
This tutorial is meant to describe:
How to create a node.js app that calls an iFlow endpoint from Cloud Foundry.
How to use sap-managed client certificate from service binding
How to fetch JWT token and CSRF token
How to handle user roles and what’s happening behind the scenes.
How to do credentials rotation for service binding.
Used Technologies:
SAP Business Technology Platform (BTP), Cloud Foundry
CF Command Line Client
Node.js
XSUAA

Quicklinks:
Quick Guide
Sample Code

Content

0.1. Prerequisites
0.2. Preparation
0.3. Introduction:
1. Create User Roles
2. Create Service Instance
3. Create Node.js App
4. Create iFlow
5. Run
6. Optional: View the JWT token
7. Credentials Rotation
8. Free Bonus Tutorial about Roles
9. Clean Up
Appendix: Sample Application Code

0.1. Prerequisites

  • There are already great blog posts in the Community that introduce into the Inbound HTTP scenario using client certificate: Mandys and Danils Blogs
  • For remaining open questions I recommend the Security Glossary.
  • To follow this tutorial, access to a Cloud Integration tenant is required, as well as basic knowledge about creating iFlows.
  • You should be familiar with Node.js, even though 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

In the previous tutorial we’ve learned how to deal with an own, custom, customer-specific client certificate and how to use it to call a simple iFlow endpoint.
Please refer to the intro of that post.

Today we’re using similar app and similar iFlow, but with different focus.
We want to use a SAP-managed client certificate.
This means, we get a newly generated fresh certificate and private key, which is handed over to us from the service instance of CPI runtime.
To get a hold of it, we can create a service key.
But the usual way for app-development in BTP: when binding an application to a service instance, the binding-information is written to the environment variables of the application. As such, the app can read the required info (credentials) at runtime and nothing needs to be hardcoded or copy&pasted.

When declaring the binding, we can specify a few parameters that are supported by the CPI service broker, to control the type and lifecycle of the credentials.

Such a SAP-managed certificate in service binding is easy to rotate.

In the previous tutorial, we used the client certificate to directly call the iFlow endpoint.
Today we want to use the client certificate to fetch a JWT token, then we use the token to call the iFlow.

In addition, we configure our iFlow to require a valid CSRF token, so we need to fetch it in the code.

Furthermore, we want to configure authorization model for our iFlow, such that a request is only allowed with a certain user role.
Today, we’re going to rely on client-credentials, it is an app-to-app scenario without user nor user-login. Nevertheless, the endpoint requires a (user) role.

Remember? In the previous blog intro we distinguished 6 options for calling iFlow:
– Bearer Token, fetched via basic auth (clientid/secret)
– Bearer Token, fetched via client certificate
– Basic Auth, sending clientid/secret to iFlow
– Client Certificate (SAP-managed) sent directly to iFlow
– Bearer Token, fetched via external certificate
– External Certificate sent to iFlow

From these 6 scenarios we choose one for today’s tutorial:
💠💠💠 Second: Bearer Token, fetched via client certificate (SAP-managed) 💠💠💠

Below diagram shows the setup for today:

We can see that our app has a binding to cpi runtime service which provides the credentials for contacting XSUAA to fetch a token.
Prior to calling the iFlow-endpoint, we fetch the required token ourselves.

Disclaimer:
Below code and design is not an official recommendation.
It is just my personal learning which I like to share with the community.

1. Create User Roles

First of all, we create some user roles in CPI -> Monitor Artifacts -> Manage Security -> User Roles
We “Add” 2 roles like:

🔹Ntif.Read
🔹Ntif.Write

The dashboard allows to download or copy the roles in json format, which is useful for the next step.

2. Create Service Instance

Now that the roles are available, we can use them in our new service instance.
To do so, we create a new file called config.json with the following content:

{
	"grant-types":[
		"client_credentials"
	],
	"roles":[
		"Ntif.Read",
		"Ntif.Write" 
	]
}

What are we doing here?
Basically, when creating a service instance, an OAuth client is issued.
With above configuration, we’re saying that we want this client to own these 2 roles.
This means, when the client asks the Authorization Server (XSUAA) for a JWT token, this token will contain these roles (scopes)

 

To create the service instance, we run the following command from our project directory:

cf cs it-rt integration-flow iflwithroles -c config.json

Alternatively, the service instance can be created using the wizard in BTP and above content can be pasted during creation.

Note:
How to find out the content?
The list of supported grant types is in the docu.
However, note that we cannot copy&paste the values for grant types from the documentation into the JSON. In the docu, we have human readable names, not the technical names.
Trick:
Copy the values from cockpit: start creating service instance, then on params page select everything, then click on JSON Tab.

Result:
Along with the cpi-service-instance, an OAuth client has been created, configured with 2 roles.
Whenever we use this client for fetching a JWT token, the token will contain these roles.
More precise: it will contain the scopes wrapped by these roles.
More info in chapter 8.
Try it.
You get a free bonus tutorial.
Only today!

3.  Create Node.js Application

Today we want to create an application that is bound to an instance of process integration, reads credentials from the binding, uses them to fetch a JWT token which is then used to call an iFlow.
As usual, everything is as simple as possible.

🔷 manifest.yml

In the manifest we specify the name of our app: ntif (node to iflow)
But more important, we configure the binding parameters for the service instance of cpi runtime
1. binding param to tell that we use SAP-managed certificate
2. binding param to specify the expiration in days
3. binding param to specify the size of the key, bigger means more secure (while slower)

applications:
  - name: ntif
  . . .
    services:
      - name: iflwithroles
        parameters:
          key-type: certificate
          validity: 7
          key-length: 4096

The full file content can be found in the appendix.

🔷 server.js

The app does nothing than call the endpoint which we will define in the iFlow below.
The iFlow endpoint is protected, so we need credentials.
The credentials are provided by the service broker of the cpi runtime instance, created before.
The credentials are handed over to us in the binding, they are written into the standard environment variable with name VCAP_SERVICES
We use a helper lib that finds the relevant entry in the environment, based on the instance name:

xsenv.serviceCredentials({ name: 'iflwithroles' })

Note:
Make sure to adapt the snippet if you’ve chosen a different name while creating the service instance.

The target url can be read from binding as well.
The credentials for calling the iFlow are client certificate and private key.
Instead of using them to directly call the iFlow, we use them to fetch a JWT token:

async function fetchJwtToken(creds){
    const options = {
        url: creds.tokenurl ,
        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
        method: 'POST',
        data : `grant_type=client_credentials&response_type=token&client_id=${creds.clientid}`,
        httpsAgent : new https.Agent({
            cert: creds.certificate,
            key: creds.key
        })
    }

    const result = await axios(options)
    const data = result.data

    return data.access_token    
}

We can see that we’re processing the OAuth flow “client-credentials” and we’re authenticating with client certificate and private key.

The application has some more work to do now:
Fetch CSRF token which is required for POST requests and configured in iFlow.

What is a CSRF token?

CSRF stands for Cross Side Request Forgery and is a hacker attack that tries to execute a hidden malicious request in the name of a user.
The server which receives the malicious request should think that it is done by the user.
To prevent such a malicious request, that originates from a different site, while using same browser, the receiving server uses the CSRF token.
A CSRF token is like a guid or hash code (see screenshot below) and is generated and bound to a user session, while not stored in the session.
The server would require such token for requests that modify data, like POST, PUT, DELETE requests.

In our example scenario, our iFlow is invoked via POST request and is configured to require a CSRF token.
As such, we have to send a request to the server to fetch such a token, as follows:

async function fetchCsrf(jwt) {
    const options = {
        url: `${CREDENTIALS.url}/http/ntif`,
        headers: {
            'Authorization': 'Bearer ' + jwt,
            'x-csrf-token': 'fetch'
        },
        method: 'HEAD'
    }
    
    const response = await axios(options)
. . .

It must be a valid request, so we have to send a valid JWT token.
We use HEAD request, which is faster.
We specify a header that tells the server to include the token in the response.

    return {
        'csrfToken': response.headers['x-csrf-token'],
        'cookie': response.headers['set-cookie'].toString()  
    }

Note:
Our helper function that fetches a CSRF token returns not only the token, but also the set-cookie.

What is a set-Cookie?
This is a header name which is used to send a cookie from the server to the user agent.
In our example, it contains some data like the sessionID and “Secure” and “HttpOnly” flags.
Such a cookie can be sent back to the server.

We need this mechanism, because for each session, a new CSRF token is generated by the server.
And for our app-to-app scenario, each request is a new session which means that for each request a new csrf token is generated.
As such, we need to store the cookie and send it to our iFlow along with the csrf token.
To send the cookie, a header with name “Cookie” is used (see below).
Like that, the next request will re-use the same session and the CSRF-token will still be valid.

Note:
If we would fetch a new CSRF token and we would include the cookie in the request, then we would get the same CSRF token, because it would be the same session.

The attribute “HttpOnly” means that the cookie cannot be accessed by JavaScript
The attribute “Secure” ensures that the cookie can only be sent to the server in HTTPS requests.
Below screenshot shows an example of how a CSRF token and a set-Cookie may look like:

After fetching JWT token, CSRF token and cookie, we can call the iFlow endpoint:

    const options = {
        url: `${CREDENTIALS.url}/http/ntif`,
        headers: {
            'Content-Type': 'application/text',
            'Authorization': 'Bearer ' + jwt,
            'x-csrf-token': csrf,
            'Cookie': cookie
        },
        method: 'POST',
        data : "Some Data sent from node application in BTP Cloud"
    }
    
    const response = await axios(options)

Our request includes the 3 requirements:
JWT token in the “Authorization” header
CSRF token in the “x-csrf-token” header
The cookie in the “Cookie” header

Note that HTTP request headers are case insensitive.

The base URL of our iFlow endpoint doesn’t have to be copied over from the “Manage Integration Content” screen of CPI, it can be accessed from the binding, as shown above.
We need to append the constant segment /http and the endpoint name that we configured in the HTTPS adapter.

That’s it.
Everything is executed from the implementation of the endpoint of our node app:

app.get('/callcpi', async (req, res) => {
    const jwtToken = await fetchJwtToken(CREDENTIALS)
    const {csrfToken, cookie} = await fetchCsrf(jwtToken)
    const result = await callCPI(jwtToken, csrfToken, cookie)

The full sample code can be found in the appendix.
Note that the code is not ready for productive usage, it doesn’t contain any error handling, etc

🔷 Deploy

After deploying the app with cf push, we read the binding in order to obtain the info about the client certificate:
cf env ntif

We need find the certificatedetails section of the cpi binding:

We take a note of the subjectdn and the issuerdn

DN stands for “Distinguished Name” and is a combination of attributes with more or less personal information about the owner and issuer of the certificate.
A DN is a kind of unique name.
The subject is the owner (our ntif app), the issuer is the authority that has issued and signed the client certificate.
In case of a chain, there are 1 or more intermediate issuers.

Note:
We can see that indeed, the certificate expires after 7 days (as specified in binding params)

Note
The SAP-managed client certificate has a subject DN like this:

"CN=123-abc-123, L=uaa, OU=yyy-111, OU=eu, OU= Cloud Platform, O=SAP, C=DE

The CN (Common Name) is the guid of the binding (or service key).
To find out the guid of the service key:
cf service-key cpiInstance sk –guid
To find out the guid of the binding:
cf env ntif and find an attribute called binding_guid

The OU (Organizational Unit) is the guid of the subaccount which can be found in the overview screen of the BTP Cockpit.

4. 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 /ntifext
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 “Ntif.Write”
Note that file extension .pem is not supported here
🔸 CSRF
Checkbox enabled.

Troubleshooting:

🔸It is not possible to write 2 roles here, as it leads to an error
🔸If we choose the default role ESB.Messaging, we get a 403 error at runtime,
because we didn’t use it when configuring our service instance
🔸If we configure “Client Certificate” here, then we get an error response (403)
🔸If we require csrf token but don’t send it, we get an 403

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

5. Run Scenario

To run our scenario, we invoke our app which will then trigger the iFlow and print the response of the iFlow in the browser window.
In my example, the app has the following url:
https://ntif.cfapps.eu.hana.ondemand.com/callcpi

The call should be successful.
We see the text which we send from our app to iFlow, in the response in the browser window.
All fine.

6. Optional: View the JWT token

We’re using the client-credentials flow to fetch a JWT token.
The JWT token is a JSON object that is Base64 encoded.

What is Base64?

Base64 is a set of “normal” characters only: alphabet upper and lower case, numbers and 3 others => 64
As it doesn’t contain strange characters, it is used to safely transfer information over the wire.
“Encoded” means just converted, so it is easy to decode it and print its content, like this:

async function fetchJwtToken(creds){
    . . .
    const result = await axios(options)

    const token = result.data.access_token
    const jwtBase64 = token.split('.')[1]
    const jwtDecodedAsString = Buffer.from(jwtBase64, 'base64').toString('ascii')

    console.log("===> The JWT: " + jwtDecodedAsString)
    return token    
}

After running the scenario, we can view the content of the JWT token in the Cloud Foundry log:
cf logs ntif –recent or cf logs ntif

The result:

{
   "jti":"0f4c85c671714a",
   "ext_attr":{
         "enhancer":"XSUAA",
         "subaccountid":"4b4d064a-5350-48df",
         "zdn":"mysubdomain",
         "serviceinstanceid":"7747130f-78e9-4d32"
      },
   "cnf":{
      "x5t#S256":"JQt2MYVN-esQgKkX"},
   "sub":"sb-7747130f-78e9!b78007|it-rt-mysubdomain!b1806",
   "authorities":[
      "uaa.resource",
      "it-rt-mysubdomain!b1806.Ntif.Write",
      "it-rt-mysubdomain!b1806.Ntif.Read"
   ],
   "scope":[
      "uaa.resource",
      "it-rt-mysubdomain!b1806.Ntif.Write",
      "it-rt-mysubdomain!b1806.Ntif.Read"
   ],
   "client_id":"sb-7747130f-78e9!b78007|it-rt-mysubdomain!b1806",
   "cid":"sb-7747130f-78e9!b78007|it-rt-mysubdomain!b1806",
   "azp":"sb-7747130f-78e9!b78007|it-rt-mysubdomain!b1806",
   "grant_type":"client_credentials",
   "rev_sig":"2ca2396e",
   "iat":1685719150,
   "exp":1685723350,
   "iss":https://mysubdomain.authentication.eu12.hana.ondemand.com/oauth/token,
   "zid":"a3203602-4354",   
   "aud":[
      "uaa",
      "it-rt-mysubdomain!b1806.Ntif",
      "sb-7747130f-78e9!b78007|it-rt-mysubdomain!b1806"
   ]
}

 

Info

🔸jti
The Identifier of the JWT token itself
🔸ext_attr
This “external attributes” is a section that can be filled with custom data.
Even clients can add custom data here, that needs to be transferred to the receiver.
A typical example could be the good old abap_name, when user info is sent to backend.
🔸subaccountid
The ID of the subaccount, can be found in the overview page of BTP
🔸zdn
Stands for identity zone, which is currently equal to the subdomain
🔸serviceinstanceid
In our example, it is the guid of the instance of the cpi service
It can be found with the command cf service iflwithroles –guid
🔸cnf
Stands for “Confirmation”, this is a fingerprint of the client certificate that was used to fetch the JWT token. It is only present in the JWT, when mTLS is used.
x5t#S256 means that a fingerprint of a X.509 certificate (-> x5t) was created with SHA-256 algorithm (-> S256)
🔸sub
The “Subject” of the client, it is the same as the client id.
🔸authorities
A list of permissions, in this example same as list of scopes
🔸scope
Scope Values, a list of scopes assigned to the bearer of this JWT token.
In case of human user, these are the scopes that are wrapped by user roles.
Scopes have technical names, as defined in the xs-security file. See bonus tutorial.
The “uaa.resource” scope is assigned automatically by XSUAA in case of client-credentials flow.
🔸client_id
The “Client Identifier” that can be found in the credentials section of the service instance.
The client is the OAuth-client that requests a resource and is entitled to obtain a JWT token, because it is registered at the Authorization Server (XSUAA)
In our case, the client id contains the guid of the service instance and the subdomain
🔸cid
Stands for “client id”.
🔸azp
This is the “Authorized Party”. The JWT token is issued for this receiver.
This is usually the client, so same value as client_id
🔸grant_type
The OAuth flow that was used to fetch the token.
Receiver can decide not to accept e.g. the deprecated “password” grant type
🔸rev_sig
Here a hash is transmitted, as “revocation signature”, used to revoke a JWT token, if supported by the server.
🔸iat
Stands for “issued at”
🔸exp
This is the “expiration time”
🔸iss
This is the “Issuer” of the token, in our case it is the token-endpoint of XSUAA server
🔸zid
Stands for “zone id” and is currently equal to the tenant id.
It can be found in the BPT Cockpit overview page
🔸aud
The “Audience”. The JWT token is issued for this receiver.
This is usually the xsappname of the target oauth-client.
This is required if scopes are granted from one instance of xsuaa to different instance.
The property xsappname is unique across an identity zone.
In above example, both the xsappname and the client id are written into the audience

7. Credentials Rotation 🔄

When deploying an application to Cloud Foundry, we use a deployment descriptor, the manifest.yml, to specify which services we want to use in our app code.
We bind such service to our app (more concrete: an instance of a service, created beforehand, is bound to our app).
At runtime. to use a service, we need its credentials.
Credentials are created during binding.
They are written into the environment variable that belongs to the application.
(Alternatively, for external usage, credentials are exposed in a “Service Key”.)
After an application is deployed we can view its environment and the credentials via
cf env ntif
To view the binding, we have to look at the details of the service instance:
cf service iflwithroles

Why this intro?
We want to invalidate the credentials and create new (rotate).

First approach:
To do so, we can redeploy our app.
However, this is not enough, as the binding already exists and won’t be re-created on deploy.
As such, we would have to delete our app, then deploy it again.
At this point in time, we would get new credentials.
But that’s of course not feasible in productive environment.

Second approach:
To invalidate the credentials, we just need to create a new binding.
To control the bindings, there are commands for the cloud foundry command line client, or the BTP Cockpit can be used.
If binding parameters are required, they can be specified in the manifest or in a config file that is used by the bind-command.
In our example (see manifest), we want to tell the service broker, which creates the service instance and binding, that we want a client certificate and how long it should live.
The binding-config is provided in JSON format, in a file or directly in the command line.

Do prepare

As such, in our project directory, we create a file bind.json with the following content:

bind.json

{
   "key-type": "certificate",
   "validity": 7,
   "key-length": 4096
}

It is the same content like in the manifest.yml, just the format is different.

Do rotation

First view the current credentials:
cf env ntif
As we’re using client certificate, we need to take a note of the Subject, the serial number, createdate and expiration date.
In my example:

“certificatedetails”: {
“serialnumber”: “0x4004000001887c4a0…”,
“subjectdn”: “CN=a606f414-c4b0-4c47,,. . .O=SAP SE,C=DE”,
“validuntil”: “2023-06-09”
},
“createdate”: “2023-06-02”,

Then we delete the existing binding:
cf unbind-service ntif iflwithroles
Now we can create a new binding, including the binding params:
cf bind-service ntif iflwithroles -c bind.json
Afterwards, we need to restage the app, for the changes to take effect
cf restage ntif

Note
This results in a short downtime

Now we want to confirm the change, so we check the app env

“certificatedetails”: {
“serialnumber”: “0x4005000001888843f…”,
“subjectdn”: “CN=16cb217c-0a80-4b93,…O=SAP SE,C=DE”,
“validuntil”: “2023-06-12”
},
“createdate”: “2023-06-05”,

Indeed, all the values are different.
As we know, the CN of the subject of our client certificate contains the GUID of the binding, which is now new.

Do test

Finally, we test our application:
The call is successful, no change in code or on iFlow side required.

8. Free Bonus Tutorial about Roles

This free section is meant for those who are not yet familiar with the Authorization concept of SAP BTP, which is based on XSUAA.
Small exercise.

8.1. Role Generation from CPI

First we use the CPI dashboard to create a dummy user role.

8.1.1. User Roles in CPI

We go to Monitoring -> Manage Security -> User Roles
We add a role with arbitrary name, e.g.

It is added to the list of roles created in CPI.
Fine.

8.1.2. User Roles in SAP BTP

Now we check the role in BTP
We go to our Subaccount, where the CPI is subscribed.
Then navigate to Security -> Roles.
We can filter the list for “it-rt” as “Application Name”.
There we can see that for each cpi-role, a “Role Template” and a default role has been generated.

We can see that all cpi-roles share the same application identifier:
We’ll learn now what it means.

BTW, default roles cannot be deleted.

So we’ve learned that right after user role creation in CPI, the generation has been propagated to the BTP.
Here, an admin can add the roles to a role collection which then can be assigned to users.
Alternatively, a role collection can be mapped to user groups coming from corporate identity provider.

8.2. Role Generation fron XSUAA

This chapter is meant to discover what is happening under the hood.
We want to define an authorization model manually.
We create a file with an arbitrary name, usually called xs-security.json, with the following content:

{
    "xsappname": "testxsappname",
    "tenant-mode": "dedicated",
    "scopes": [{
            "name": "$XSAPPNAME.scopeforview",
            "description": "Scope required for viewing something."
        },{
            "name": "$XSAPPNAME.scopeforcreate",
            "description": "Scope required for creating something new."
        }
    ],
    "attributes" : [ { 
            "name" : "Country", 
            "description" : "Contains country information of assigned user", 
            "valueType" : "string",
            "valueRequired" : "false" 
        }
    ],     
    "role-templates": [{
            "name": "UserRole",
            "description": "Role for end users, allows to view list of something",
            "default-role-name": "End User Viewer Role",
            "scope-references": ["$XSAPPNAME.scopeforview"]
        },{
            "name": "AssistantRole",
            "description": "Role for assistants, allows to view and create employees.",
            "scope-references": ["$XSAPPNAME.scopeforview", "$XSAPPNAME.scopeforcreate"],
            "attribute-references": [ { "name" : "Country" } ]  
        }
    ],
    "role-collections": [{
        "name": "UserRoles",
        "role-template-references": [ "$XSAPPNAME.UserRole" ]
      },{
        "name": "AssistantRoles",
        "role-template-references": [ "$XSAPPNAME.AssistantRole" ]
      }
    ]
}

We define 2 scopes and 2 corresponding role templates.
We define one attribute “Country” with no default value.
We set “value required” to false, otherwise we would  be forced to specify static default value in this file. However. we want to use the cockpit to create specific roles for specific countries.
The attribute is referenced only by the “AssistantRole” template.
The assistant role is kind of special role, because under certain circumstance (same country), it has additional capabilities. This dynamic authorization handling is possible via attribute.

We also define 2 default role collections, so the admin doesn’t need to create them.
However the “AssistantRoles” collection will contain the special (dynamic) Assistant Role without attribute value. This doesn’t make sense and we keep it for testing purpose only.

The docu can be found here.

On command line, we execute below command to create an instance of xsuaa service:
cf cs xsuaa application testXsuaa -c xs-security.json

The command refers to xsuaa service with service plan “application” and the instance is created with name testXsuaa

After service creation we can check the cockpit for the newly created roles and role collections.
Note that the page might need to be refreshed.

What do we learn here?

The value of “Application Name” is the value that we’ve given for the property “xsappname” in the security configuration file.
We can see both role templates.
We learn that a role template is really just a template that needs to be instantiated at runtime. For that, there are default roles created automatically.

In case of “UserRole”, we’ve specified a name for the default role, which we can see is displayed.
In case of “UserRole”, we can see that it is not possible to “Delete” it. This is the case because it is a default that was defined beforehand.
In case of “UserRole”, we can see that it is not possible to “Add” a role. The reason is that the default role is already generated and it doesn’t make sense to create another one, because it would be identical.

In case of “AssistantRole” it is different, because of the attribute. Means that every role.
We click on “Create Role”, enter some name and click Next
For the attribute, we choose “Static” which allows us to enter a static country name. We enter “ES” then we have to press “Enter”.
We skip the role collection and finish the wizard.
After finishing the wizard, we can see that this new role can be deleted, and also no role can be “Added”, as it is not a template.

That’s it – has been a nice learning, right?

9. Clean Up

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

🔹 Delete app
cf d -r -f ntif
🔹 Delete service instances
cf ds iflwithroles -f
cf ds testXsuaa -f
🔹 Delete User Roles in CPI (this deletes also the roles in BTP)
🔹 Delete iFlow
I’m even pasting a delete-screenshot for you…

Summary

In this blog post we’ve learned how to write an application for Cloud Foundry that calls a protected iFlow endpoint.
We’ve learned how to configure the service binding in the manifest.yml file and in the binding JSON.
We’ve used a SAP-managed client certificate to fetch a JWT token which was then send to the iFlow.
We’ve also learned quite some details about JWT tokens.
We’ve learned about CSRF protection and we’ve learned how to programmatically fetch that token.
We’ve learned how to handle user roles and also learned what’s happening in the background.
Finally, we’ve learned how simple it is to do credentials rotation in case of SAP-managed credentials.

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

Blog Posts
Mandys blog post about inbound connection.
Daniils blog post with step by step description.
My blog post about XSUAA user attributes.
Security Glossary Blog

Other
JWT specification: rfc7519
IANA JWT Claims.
Cloud Foundry UAA docu: introspect token
Node.js crypto package
Cloud Foundry CLI Installation guide

Appendix 1: Sample Application Code

Project Structure:

Note:
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.

manifest.yml

---
applications:
  - name: ntif
    memory: 64M
    path: app
    routes:
    - route: ntif.cfapps.eu12.hana.ondemand.com
    buildpacks:
      - nodejs_buildpack
    services:
      - name: iflwithroles
        parameters:
          key-type: certificate
          validity: 7
          key-length: 4096

package.json

{
  "dependencies": {
    "axios": "0.24.0",
    "@sap/xsenv": "latest",
    "express": "^4.17.1"  
  }
}

server.js

const https = require('https')  
const xsenv = require('@sap/xsenv')
const axios = require('axios')
const crypto = require('crypto')
const express = require('express')

const app = express()

const CREDENTIALS = xsenv.serviceCredentials({ name: 'iflwithroles' }).oauth 


/* App Server */
app.listen(process.env.PORT)


/* App Endpoint */

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

    // check validity of client cert
    const certificate = new crypto.X509Certificate(CREDENTIALS.certificate)  
    console.log("===> Client Certificate valid to: " + certificate.validTo) 

    // fetch jwt and csrf tokens, to call iFlow endpoint
    const jwtToken = await fetchJwtToken(CREDENTIALS)
    const {csrfToken, cookie} = await fetchCsrf(jwtToken)
    const result = await callCPI(jwtToken, csrfToken, cookie)
 
    res.send(`Received response from iFlow: '${result}'`)
})


/* Helper */

async function fetchJwtToken(creds){
    const options = {
        url: creds.tokenurl ,
        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
        method: 'POST',
        data : `grant_type=client_credentials&response_type=token&client_id=${creds.clientid}`,
        httpsAgent : new https.Agent({
            cert: creds.certificate,
            key: creds.key
        })
    }

    const result = await axios(options)
    
    // if desired, decode JWT token and print info
    const token = result.data.access_token
    const jwtBase64 = token.split('.')[1]
    const jwtDecodedAsString = Buffer.from(jwtBase64, 'base64').toString('ascii')
    console.log("===> The JWT: " + jwtDecodedAsString)
    return token    
}


async function fetchCsrf(jwt) {
    const options = {
        url: `${CREDENTIALS.url}/http/ntif`,
        headers: {
            'Authorization': 'Bearer ' + jwt,
            'x-csrf-token': 'fetch'
        },
        method: 'HEAD'
    }
    
    const response = await axios(options)
    return {
        'csrfToken': response.headers['x-csrf-token'],
        'cookie': response.headers['set-cookie'].toString()  
    }
} 


async function callCPI(jwt, csrf, cookie) {
    const options = {
        url: `${CREDENTIALS.url}/http/ntif`,
        headers: {
            'Content-Type': 'application/text',
            'Authorization': 'Bearer ' + jwt,
            'x-csrf-token': csrf,
            'Cookie': cookie
        },
        method: 'POST',
        data : "Some Data sent from node application in BTP Cloud"
    }
    
    try {
        const response = await axios(options)
        console.log(`===> Result: ${JSON.stringify(response.data)}`)
        return response.data
    } catch (error) {
        console.log("===> ERROR calling CPI: error.response.data: " + error.response.data)
        console.log("===> ERROR calling CPI: error.response.status: " + error.response.status)
        console.log("===> ERROR calling CPI: error.response.headers: " + JSON.stringify(error.response.headers))
        console.log("===> ERROR calling CPI: error.response.data.error: " + error.response.data.error)
    }
}

config.json

{
	"grant-types":[
		"client_credentials"
	],
	"roles":[
		"Ntif.Read",
		"Ntif.Write" 
	]
}

bind.json

{
    "key-type": "certificate",
    "validity": 7,
    "key-length": 4096
}

 

 

Sara Sampaio

Sara Sampaio

Author Since: March 10, 2022

0 0 votes
Article Rating
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x