Skip to Content
Technical Articles

How to call SAP BTP application, scheduled by Microsoft Azure Logic App

In SAP Business Technology Platform (BTP) you can create an application that requires to perform a recurrent task on regular basis.
This could be a cleanup of the underlying database, or some regular data replication, or any update from third-party, or test run or health check, etc etc

To do so, you can leverage the so-called Azure Logic Apps on the Microsoft Azure cloud.
Azure Logic Apps is a service that can be used like a workflow that can be started by recurrence trigger and that can be designed to call a service endpoint via HTTP.

This tutorial shows how to do so and how to overcome the hurdle of OAuth secured application endpoint.

Overview

In the first section, we create a Node.js sample application and deploy it to SAP BTP.
Afterwards me head over to Microsoft Azure and create a Logic App to call that endpoint

  1. Create app on SAP Business Technology Platform
    1. Create Project
    2. Configure Security
    3. Create Application
  2. Create logic app on Microsoft Azure
    1. Call free endpoint
    2. Configure OAuth handling
  3. Check results
    1. View Run History on Azure
    2. View logs on SAP BTP
  4. Appendix
    1. Sample app code
    2. Logic App code

Prerequisites

  • You should be little familiar with Node.js – or at least be able to ignore it.
    In this tutorial we create an app which provides an endpoint which will be called on a regular basis from a scheduled flow in Microsoft Azure. You can use any existing endpoint for this exercise.
    However, put attention on the security descriptor to make sure that the endpoint – if secured – can be invoked remotely.
  • SAP Business Technology Platform (BTP)
    Access to SAP BTP and enough quota to deploy an application to Cloud Foundry is required.
    Note:
    This tutorial is based on Cloud Foundry and XSUAA
  • Microsoft Azure
    Access to Microsoft Azure is required to follow this tutorial.
    I assume you’re reading this blog post because you’re already using Azure

 

1. SAP Business Technology Platform

In this section we create and deploy a Node.js application to Cloud Foundry environment of SAP BTP.
This application provides an endpoint which is secured with OAuth 2.0
For easier getting started, it also provides an endpoint which is not protected and can be accessed by anybody – or anything.
In a real scenario, this endpoint would probably be used for database cleanup, replication or similar professional scenarios.
In my dummy scenario, we use this endpoint for nothing.

1.1. Create Project

To create our silly dummy application, we create a project folder as follows:

C:\tmp_illogicalapp
|- manifest.yml
|- package.json
|- server.js
|- xs-security.json

For your convenience, please refer to below screenshot:

The content of the files can be copied from the Appendix

1.2. Configure Security

As mentioned, we’re going to create an OAuth protected endpoint.
As such, we need to create an instance of XSUAA in Cloud Foundry

Create instance of XSUAA

The parameters for the XSUAA instance are contained in the xs-security.json file, which can be passed during creation in the cockpit, or on command line.

To create the service instance on command line using the CF CLI, we jump into the project folder and execute the following command:

cf create-service xsuaa application xsuaa_illogical_app -c xs-security.json

Let’s have a quick look at the security configuration:

{
  "xsappname" : "xsuaa_illogical_app",
  "tenant-mode" : "dedicated",
  "scopes": [{
      "name": "$XSAPPNAME.illogicalscope"
  }],
  "authorities":["$XSAPPNAME.illogicalscope"]
}

Important to note:
We define a scope.
But this is not enough.
When the XSUAA issues a JWT token, the scope won’t be contained.
Scope needs to be assigned
Assigning a scope to a human user is done by means of roles and role collections, which are created by a cloud admin and mapped to the identity provider.
In our case, we don’t have users, we have client-credentials scenario:
To assign the scope to our present OAuth client, the authorities statement is used.
It is like assigning the scope to ourselves.
Or, with other words, like accepting our own scope.

This is very short explanation, for more details, please go through the following blog post.

Create Service Key

After creating an instance of XSUAA, we can bind our application to it and use the binding for securing one of our endpoints (see next step).
However, we need credentials of the OAuth client to be available externally.
Why?
This is necessary if we want to call the protected endpoint e.g. from a local REST client, like postman.
Or from a test environment.
Or from Microsoft Azure Logic App.

To get explicit service credentials, we create a Service Key.
This can be done in the cockpit, or with the following command:

cf csk xsuaa_illogical_app service_key_illogic

Afterwards we need to view the credentials.
To view the content of the service key, either use the cockpit or the following command.

cf service-key xsuaa_illogical_app service_key_illogic

From the bunch of information, we need to take a note of the following 3 properties:

“clientid”: “sb-xsuaa_illogical_app!t12345”
“clientsecret”: “12345abcdABCDk73suGmbk9BITs=”
“url”: “https://<subacc>.authentication.eu10.hana.ondemand.com”

We’ll need these values below.

1.3. Create Application

Our application which is meant to be called from Azure Logic Apps is a tiny silly Node.js app, representing a real application that exposes one or more REST endpoints which should be invoked on regular basis.
Our app exposes one protected and one free endpoint.

Let’s have a brief look:

The free endpoint returns just a silly text, and an info about who has called it:

app.get('/free', function(req, res){      
   . . .
   res.send(`===> Free endpoint successfully invoked from ${req.headers['user-agent']}. Available scopes:  ${scope}`);
});

We use it only for a first test.

The protected endpoint uses the node modules passport and @sap/xssec for protection and in addition, it checks for valid scope in the JWT token.
In the response, we send info about the available scopes found in the JWT token and about who has called the endpoint:

const xsuaaService = xsenv.getServices({ myXsuaa: { tag: 'xsuaa' }});
passport.use(new JWTStrategy(xsuaaService.myXsuaa));

app.use(passport.authenticate('JWT', { session: false }));
app.get('/prot', function(req, res){      
   . . . 
   if(req.authInfo.checkScope(MY_SCOPE)){
      res.send(`endpoint called from ${req.headers['user-agent']}, scope : ${jwtDecodedJson.scope}`);
    . . . 

In a professional application, we would define a dedicated scope only for the logic app, to ensure that the workflow cannot invoke other, more sensitive endpoints.

Deploy app

To deploy the app, we define the manifest.yml, as shown in the appendix and use the command cf push

Afterwards we can test our app endpoints

https://illogicalapp.cfapps.eu10.hana.ondemand.com/free

and

https://illogicalapp.cfapps.eu10.hana.ondemand.com/prot

The results are as expected, second endpoint throws error.

2. Microsoft Azure

To create a Logic App, there are 4 options: using the Azure portal, visual studio, VSCode or the  CLI.
This blog post is based on the portal.
To go to the portal, we use the following link:

https://portal.azure.com/

2.1. Create Logic App

To create a resource on Microsoft Azure, there are multiple ways.
The most uncomfortable one is described here:

Open Azure Portal.
Expand the navigation pane on the left and click “All services”.

From “category”, choose “Integration” and select “Logig Apps”.

There we are:

To create a logic app, we press “Add”.
In the “Create a logic app” screen, we enter the following details:

Subscription:
Choose your subscription, in my case it is the “Free Trial”.

Resource Group:
If you don’t have one, here you can press “create new” and enter a name of your choice – like I did.

Logic app name:
Here we enter a name of our choice for this artifact which we want to run on a regular basis.

Region:
Choose the region that matches best your location.

Finally, we press “Review + create”

And press “Create” after review.

The deployment is created and after short time we can continue.

We can see our logic app is created and we can go to that logic app to configure it.

To do so, we have convenient way: we can click on the button “Go to resource”.
But we also have the inconvenient way (which we obviously choose):

Navigation pane -> Home -> All services -> Integration -> Logic Apps 

In the list of logic apps, we click on the newly created one.
Since it isn’t configured yet, the Logic Apps Designer is opened directly.
We don’t want to use a template, so we choose “Blank Logic App”.

Now the Logic App Designer is opened and ready for designing a flow.

Flows are designed by adding steps and combining them to a flow.
Steps can be configured to pick the result of the previous step.

2.2. Call free endpoint

Our first try to design a flow is based on our free endpoint, which makes the flow much simpler and gives us a quick win result.

2.2.1. Trigger

The first step is to define a trigger.
In our scenario, we want to invoke our endpoint on a regular basis, e.g. every night
So what we need is a trigger with the intuitive name “Schedule”.

To add the trigger, we select the tab “Built-in”, then click on “Schedule”, then “Recurrence”
(yes, there’s a faster way…)

Once the initial trigger-step is chosen, we have to configure it.
In our fist silly example, we choose the Frequency as “Week” (to avoid cost. That can be changed later).

2.2.2. Add Action

So what should happen, once our flow is triggered by the schedule?
We want it to call our service endpoint on the SAP BTP.

So we need to define the next step by clicking on “New step”
To choose the desired connector, this time we enter “HTTP” in the search field.
Then we select the HTTP connector and click again HTTP to choose the corresponding action.
Now we enter the following configuration data:

Method:
GET

URI:
our free endpoint of the app deployed to SAP Business Technology Platform.
In my example, I’ve entered:
https://illogicalapp.cfapps.eu10.hana.ondemand.com/free

That’s already it, for our first flow.

We can click the “Save” button in the upper left corner.

2.2.3. Test

We don’t need to wait for the schedule to activate the flow.
We can press the “Run” button in the toolbar to manually trigger the flow.
After few seconds we get the result:

To view details, we click on the bar, to expand it:

We can see the success-status-code and we can see the user-agent header in the response of our endpoint.

After checking the result, we click the button “Designer” to continue editing our logic app.

2.3. Call protected endpoint

To call the protected endpoint, we need to send a valid JWT token along with the request.

2.3.1. Trigger

The first step, the schedule-trigger, remains unchanged.

2.3.2. Change HTTP step to fetch jwt

To fetch the JWT token, we change the HTTP-step which we created before.

We can fetch the JWT token using the credentials from the service key which we noted in the beginning of this tutorial.
Remember?

“clientid”: “sb-xsuaa_illogical_app!t12345”
“clientsecret”: “12345abcdABCDk73suGmbk9BITs=”
“url”: “https://<subacc>.authentication.eu10.hana.ondemand.com”

The step is then configured as follows:

Method:
POST

URL:
composed from the url property from service key with appended endpoint for token issue:
<url>/oauth/token
In my example:
https://subacc.authentication.eu10.hana.ondemand.com/oauth/token

Headers:
We enter the key and value as following strings:
Key:    Content-Type
Value: application/x-www-form-urlencoded

Body:
In the request body we send a string which is composed as described here:

grant_type=client_credentials&client_id=<propertyclientid>&client_secret=<propertysecret>

Make sure to replace the values of <propertyclientid> and <propertysecret>
In my example:

grant_type=client_credentials&client_id=sb-xsuaa_illogical_app!t12345&client_secret=12345abcdABCDk73suGmbk9BITs=

 

That’s it about configuring the jwt fetch step.
However, before we continue with the next step, we need to execute the current flow, to get a hold of the token-response.

So we click “Save”.
Then click “Run”.

In the result, we can see the response of the HTTP request.

We need to take a note of the complete response:

{
  "access_token": "ey...hello...cu...byebye",
  "token_type": "bearer",
  "expires_in": 43199,
  "scope": "uaa.resource xsuaa_illogical_app!t22273.illogicalscope",
  "jti": "12345abcdebb491087c96cf345d1755f"
}

To continue with the next step, we switch back to the designer.

2.3.3. Add parse step

The response of the HTTP call to fetch the JWT token is a long string. A string which is structured like a JSON object.
To extract the property access_token, we have to parse the string to a JSON object.
This can be done in the next step.

We press “New Step”.
We enter “parse json” in the search field.
We select the “Parse JSON” action.

To configure the parser we need to enter content and schema.
The content is the response body of the previous step.
But how to get the value from previous step?

We click into the field, then a dialog is opened which allows to “Add dynamic content”.
There, we can choose “Body”:

As a result, a variable is inserted into the Content field
That’s what we want.

Next, the parser needs the Schema in order to ingest the properties.
We don’t have the Schema, but we can click on “Use sample payload to generate schema”.
After clicking on “Use sample payload…” a dialog is opened where we can enter our sample payload.
Which sample payload?
This is the long response-string from the previous step, which we had noted above.

After pasting the response string from the JWT-fetch into the dialog, we can press “Done” and the schema is generated.

That’s it for this step.

2.3.4. Add HTTP step to call endpoint

We press “New step” and choose the HTTP action again, like described above.
This time, we configure it to call the protected endpoint and to send the parsed access token in the authorization header:

Method:
GET

URL:
This time we enter the protected endpoint.
In my example:

https://illogicalapp.cfapps.eu10.hana.ondemand.com/prot

Headers:
We pass the authorization as header.
Key: Authorization
The value: Bearer <access_token>

The access token has to be taken from the result of the previous step.
So again we click into the field “Enter value”, to see the “Add dynamic content” dialog displayed.
From there, we can choose the property access_token.

Note:
There must be a blank between the text “Bearer” and the access_token property

That’s it, we can save.

2.3.5. Test

We click on “Run” and the result should show that nice little green tick on every step.
In the details we can see the response of our protected endpoint.

That’s it.

We have created a recurrent schedule which successfully calls an OAuth-protected endpoint and gets the result.

2.3.6. More

What else?

Step by step
It would make sense to define a reaction, depending on the result of the previous HTTP call,
e.g. raise an alert.
But that’s not topic of the present blog post.

Code
An interesting feature is the access to the underlying JSON representation of the flow.
To view it, click on “Code view” in the Logic Apps Designer:

For your convenience, I’ve copied the code representation of our logic app into the appendix section

Security
The flow might disclose too much sensitive information. So for security considerations on Microsoft Azure Logic Apps, please check this page in the concepts section of the Azure documentation.

Cost
After going through this tutorial, don’t forget to disable or delete the logic app in order to avoid unnecessary billing.
The “Disable” button can be found on the overview screen of each Logic App.
E.g.
Home->All services -> Integration -> Logic apps -> CallSapEndpoint

3. View Result

On Microsoft Azure side, we’ve already seen the response of the called application in the Logic App Designer.
In addition, we can view the run history.
It can be found on the overview screen of one selected logic app:

On SAP BTP side, we can check the illogical logs that have been written by our deployed sample application.
To view the logs on command line, we can execute the following command:

cf logs illogicalapp –recent

In the logs, we can see that the endpoint has been invoked by “azure-logic-apps” and that the required scope has been sent:

Summary

In this blog post we’ve tried to use a hyperscaler to call an application on SAP Business Technology Platform.
The intention was to create a Logical App on Microsoft Azure that calls a secured application on SAP BTP.
Logical Apps allow to design a flow that is triggered e.g. by a Schedule.
Within that flow it is possible to fire HTTP requests and parse JSON response, such that a valid JWT token can be fetched from XSUAA service in SAP BTP.

Like that, we can schedule jobs on Microsoft Azure which call a service endpoint on SAP BTP on a regular basis.

Disclaimer

Please consider that the present blog post is not an official documentation nor recommendation.
I’m only describing what I found out.
There’s no guarantee that things will always work as described and look like shown in the screenshots.
Please accept my apologies…

Links

Appendix 1 : All Sample Project Files

To follow the tutorial, you can use the following files to create an OAuth protected application on SAP Business Technology Platform, Cloud Foundry environment.
You need Node.js to run the app locally. However, if you only need to deploy, it is not needed to install the dependencies with npm install, or to run the app locally.

manifest.yml

---
applications:
- name: illogicalapp
  path: .
  memory: 128M
  buildpacks:
    - nodejs_buildpack
  services:
    - xsuaa_illogical_app

package.json

{
  "main": "server.js",
  "dependencies": {
    "@sap/xsenv": "latest",
    "@sap/xssec": "latest",
    "express": "^4.16.3",
    "passport": "^0.4.1"
  }
}

xs-security.json

{
  "xsappname" : "xsuaa_illogical_app",
  "tenant-mode" : "dedicated",
  "scopes": [{
      "name": "$XSAPPNAME.illogicalscope"
  }],
  "authorities":["$XSAPPNAME.illogicalscope"]
}

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)
passport.use(jwtStrategy);

const app = express();

// Middleware to read JWT 
function jwtLogger(req, res, next) {
   console.log(`===> [LOGGER]: user-agent header: ${req.headers['user-agent']}`)
   console.log('===> [LOGGER]: Decoding JWT...');
   
   let jwtDecodedJson = decodeJwt(req)
   if(jwtDecodedJson){
      console.log('===> [LOGGER]: JWT: scopes: ' + jwtDecodedJson.scope);
      console.log('===> [LOGGER]: JWT: client_id: ' + jwtDecodedJson.client_id);
      console.log('===> [LOGGER]: JWT: audience: ' + jwtDecodedJson.aud);
   }
   next()
}

app.use(jwtLogger)

app.get('/free', function(req, res){      
   console.log('===> Free endpoint invoked')
   let jwtDecodedJson = decodeJwt(req)
   let scope = ""
   if(jwtDecodedJson){
      scope = jwtDecodedJson.scope
   }
   console.log(`===> JWT: scopes:  ${scope}`);
   res.send(`===> Free endpoint successfully invoked from ${req.headers['user-agent']}. Available scopes:  ${scope}`);
});

// configure express server with authentication middleware
app.use(passport.initialize());
app.use(passport.authenticate('JWT', { session: false }));

// app endpoint with authorization check
app.get('/prot', function(req, res){      
   console.log('===> Protected endpoint invoked')

   let jwtDecodedJson = decodeJwt(req)
   console.log(`===> JWT: scopes:  ${jwtDecodedJson.scope}`);

   const MY_SCOPE = xsuaaCredentials.xsappname + '.illogicalscope'
   if(req.authInfo.checkScope(MY_SCOPE)){
      res.send(`The protected endpoint was properly called from ${req.headers['user-agent']}, the required scope has been found in JWT token: ${jwtDecodedJson.scope}`);
   }else{
      return res.status(403).json({
         error: 'Unauthorized',
         message: 'The endpoint was called by user who does not have the required scope: <illogicalscope> ',
     });
   }
});

const port = process.env.PORT || 3000;
app.listen(port, function(){})

// helper

function decodeJwt(req) {
   let authHeader = req.headers.authorization;
   if (authHeader){
      var theJwtToken = authHeader.substring(7);
      if(theJwtToken){
         console.log('===> [LOGGER] the received JWT token: ' + theJwtToken )
         let jwtBase64Encoded = theJwtToken.split('.')[1];
         if(jwtBase64Encoded){
            let jwtDecoded = Buffer.from(jwtBase64Encoded, 'base64').toString('ascii');
            return JSON.parse(jwtDecoded);
         }
      }
   }else{
      console.log('===> no authorization header')
   }
}

Appendix 2: Logic App Code

{
    "definition": {
        "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
        "actions": {
            "HTTP": {
                "inputs": {
                    "body": "grant_type=client_credentials&client_id=sb-xsuaa_illogical_app!t22273&client_secret=800qAV7WaEtGwk73suGmbk9BITs=",
                    "headers": {
                        "Content-Type": "application/x-www-form-urlencoded"
                    },
                    "method": "POST",
                    "uri": "https://georelations.authentication.eu10.hana.ondemand.com/oauth/token"
                },
                "runAfter": {},
                "type": "Http"
            },
            "HTTP_2": {
                "inputs": {
                    "headers": {
                        "Authorization": "Bearer @{body('Parse_JSON')?['access_token']}"
                    },
                    "method": "GET",
                    "uri": "https://illogicalapp.cfapps.eu10.hana.ondemand.com/prot"
                },
                "runAfter": {
                    "Parse_JSON": [
                        "Succeeded"
                    ]
                },
                "type": "Http"
            },
            "Parse_JSON": {
                "inputs": {
                    "content": "@body('HTTP')",
                    "schema": {
                        "properties": {
                            "access_token": {
                                "type": "string"
                            },
                            "expires_in": {
                                "type": "integer"
                            },
                            "jti": {
                                "type": "string"
                            },
                            "scope": {
                                "type": "string"
                            },
                            "token_type": {
                                "type": "string"
                            }
                        },
                        "type": "object"
                    }
                },
                "runAfter": {
                    "HTTP": [
                        "Succeeded"
                    ]
                },
                "type": "ParseJson"
            }
        },
        "contentVersion": "1.0.0.0",
        "outputs": {},
        "parameters": {},
        "triggers": {
            "Recurrence": {
                "recurrence": {
                    "frequency": "Week",
                    "interval": 3
                },
                "type": "Recurrence"
            }
        }
    },
    "parameters": {}
}

1 Comment
You must be Logged on to comment or reply to a post.
  • Hi Carlos Roggan ,

    thank you for your excellent blogs which provides insider view and hands on experience to these usefull topics.

    I dont finished the blog yet, but one remark - if I tried to push app to BTP trial account, it failed because there is illogicalapp already existing - probably yours one. I have to rename the app in manifest to push it succesfully. Trial accounts probably shares namespace for apps and it has to be unique among all trial accounts.

    EDIT1: The second finding - in a step where you are defining call for getting oauth token, there you define content type as url encoded www form. I spent half an hour finding why I got always 401 unauthorized / bad credentials when I trigger run the logical app. 

    It was because that client_id and client_secret have to url encoded. For some reason, that azure client dont do url encoding before sending the request. So if you have special characters like =,+ or /, then you have to url encode that string by yourself. So 3ObGQyKZCQ+0hK8/TMmw76PUkZ0= becomes 3ObGQyKZCQ%2B0hK8%2FTMmw76PUkZ0%3D