The goal of this blog post is to describe a scenario where an application running on
SAP Business Technology Platform (BTP, aka SAP Cloud Platform) can be scheduled to run from
Amazon AWS.
The scenario which I tried and which I’d like to share with you is the following:
A scheduled event triggers an
AWS Lambda Function which is implemented to call a protected REST endpoint of an app deployed on
SAP BTP
Quicklinks:
Sample application
Sample Lambda
Prerequisites
To follow this tutorial, the following prerequisites are required:
- Access to SAP Business Technology Platform (aka SAP Cloud Platform)
Trial account is sufficient
Basic knowledge about developing applications
- Access to Amazon AWS cloud
Free Tier is sufficient
- Basic knowledge about Node.js
However, it is not necessary to run the app locally, so Node.js doesn't need to be installed.
Overview
As mentioned, our scenario consists of 3 components:
Scheduled Event
-> Lambda
-> Application
In our tutorial, we start from the end: the target app.
Then we create a
Lambda function which calls the app.
Then we define a schedule which triggers the
Lambda.
All lines of code can be found in the
Appendix section at the end of this blog post.
- Create app on SAP BTP
- Create Lambda on AWS
- Define schedule on AWS
- Check results
- Optional: Homework
1. Create app on SAP BTP
As mentioned, we start from the end: the target application on
SAP Business Technology Platform.
If you already have an application, you can use it.
The only requirement: your app should provide a service endpoint which can be called via HTTP.
For our tutorial, we create a new application.
To make the scenario a bit more interesting, we protect the endpoint with OAuth 2.0.
In addition, we define and enforce a dedicated scope for that endpoint.
1.1. Create Project
Trying to keep this section short.
We create the following folder structure on our local file system.
C:\tmp_sappapp
|- manifest.yml
|- package.json
|- server.js
|- xs-security.json
See screenshot:

Afterwards, we copy the content of the files from the
Appendix
1.2. Configure Security
Our app provides an endpoint which is dedicated to be invoked on regular basis.
It should only be invoked by scheduled machine, as such it is protected with OAuth and it requires a scope which must be assigned to the calling job.
To handle security on SAP BTP, we use XSUAA in Cloud Foundry.
1.2.1. Create instance of XSUAA
We create an instance of XSUAA and configure it with parameters which are contained in the
xs-security.json file.
It 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_sappapp -c xs-security.json
Let’s have a quick look at the security configuration:
{
"xsappname" : "xsuaa_sappapp",
"tenant-mode" : "dedicated",
"scopes": [{
"name": "$XSAPPNAME.sappappscopp"
}],
"authorities":["$XSAPPNAME.sappappscopp"]
}
Note:
Above we define a scope.
Our application will check the incoming JWT token, to enforce that scope.
But how to assign the scope to the caller in client-credentials scenario?
Later on, we will use this instance of XSUAA to obtain a JWT token.
We want this instance to add the scope to that token.
For that purpose, we have to add the
authorities statement.
Note:
We don’t define any role (on top of the scope).
This ensures that no human user can get the permission to call the endpoint.
This is very short explanation, for more details, please go through the following
blog post.
Note:
After reading my other blog post, you might wonder, why we don’t create an additional instance of XSUAA which we use only for AWS Lambda and to which we “grant” the scope.
Yes, good catch, this is a good alternative, but it would make this blog post longer while not adding much benefit for the focused scenario.
If you like, please try it on your own and share your experience in the comments section
1.2.2. 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).
During binding, the credentials of the service instance are made available to the app.
Credentials can be used to call the XSUAA authorization server to obtain a JWT token.
But in our case, we don’t need the credentials inside the app.
We need them externally.
Credentials are necessary e.g. if we want to call the protected endpoint from a local REST client, like
postman.
Or from a test environment.
Or from an
AWS Lambda function
.
To get explicit service credentials, we need to create a
Service Key.
This can be done in the cockpit, or with the following command:
cf csk xsuaa_sappapp service_key_sappapp
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_sappapp service_key_sappapp
From the bunch of information, only these 3 properties are of interest for us:
"clientid": "sb-xsuaa_sappapp!t12345"
"clientsecret": "12345abcdABCDk73suGmbk9BITs="
"url": "https://<subacc>.authentication.eu10.hana.ondemand.com"
We take a note of the values, because we'll need them later, when creating our Lambda function.
1.3. Create Application
We quickly create a small Node.js app which exposes a REST service endpoint.
The implementation is not interesting, as it does nothing.
Let’s just have a brief look:
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}`);
. . .
The endpoint uses the node modules
passport and
@Sisn/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.
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 endpoint
https://sappapp.cfapps.eu10.hana.ondemand.com/prot
in a browser window, just to see that it fails with the expected error
Unauthorized with code
401
2. Create Lambda Function on AWS
In the first step, we created an application and deployed it to
SAP Business Technology Platform (BTP) and we secured it with OAuth 2.0
We failed to call it with browser because we have to follow the OAuth flow.
Now we switch to
Amazon Web Services (AWS) and create an
AWS Lambda function with calls the REST service endpoint of that application.
Since the endpoint is protected with OAuth 2.0, the new function has to do the OAuth flow.
In order to do the OAuth flow, the function has to use the credentials which we received when we created the Service Key for the XSUAA service instance (step 1.2.2.).
2.1. Create Lambda Function
In this section, we enter the AWS portal and create a Lambda using the dashboard in our browser.
To enter the
AWS Management Console, we can use the following URL:
console.aws.amazon.com
To open the Lambda Console, we go to
All Services -> Compute -> Lambda

Alternatively, the direct link:
console.aws.amazon.com/lambda
In the Lambda console, we can press “Create Function”

We enter the following details:
We choose “Author from scratch”,
enter function name: “callSappApp”
and select a current version of runtime: “Node.js xx”

Finally we press "Create".
After few seconds, the details screen of our new function is displayed and we can can enter our function code.
We double-click the generated
index.js file, to open it in the editor:

We delete the generated sample code and replace it with the function code from the
appendix section.
The sample function code is just sample code and meant to be short. In any case, you should improve it (see
homework).
The function first calls the URL of the OAuth Authorization server on SAP BTP, Cloud Foundry, XSUAA, to get a valid JWT token.
In a second step, it calls the endpoint of our Node.js application, deployed on SAP BTP
The function just returns the response of the app, its status code and the response body.
. . .
exports.handler = async (event) => {
const jwtToken = await _fetchJwtToken()
const result = await _callSapEndpoint(jwtToken)
return `Function called SAP app which responded: [status ${result.status}] '${result.message}'`
};
. . .
Note:
Make sure to replace the placeholders with the data retrieved from your service key
const ENDPOINT_HOST = 'https://sappapp.cfapps.eu10.hana.ondemand.com'
const ENDPOINT_PATH = '/prot'
const OA_URL = 'https://<subaccount>.authentication.eu10.hana.ondemand.com'
const CLIENT_ID = 'sb-xsuaa_sappapp!t11111'
const CLIENT_SECRET = 'yourclientsecret'
After pasting and adapting the function code, we save the code via
File -> Save from the menu of the Function code editor.
Then we press the “Deploy” button.
2.2. Test run the Lambda function
To run the function, we press the “Test” button.
We’re asked to define a test event.
We can leave the default settings, because we don’t have any requirements to the event. Our event will be just a schedule which triggers our function. In our tutorial, we don't need any parameters.
We only need to enter a name of our choice.
And press “Create”.

After the test event is created, we can finally run the “Test”.

A new tab is opened which gives little overview on the test run of the Lambda function.
We can see the response of our function which in turn consists of the response of our little Node.js app on SAP BTP
Good.
We have the protected SAP application on SAP BTP and we have the AWS Lambda function which does the OAuth flow and successfully calls the protected endpoint of the SAP app.
This makes us already very happy.
Now we want to define a schedule, to make sure that the AWS Lambda function is triggered on a regular basis.
3. Define schedule on AWS
To define a schedule, we use
Amazon EventBridge.
Note:
An alternative option would be to use an
Amazon CloudWatch event.
The procedure is very
similar.
We open the
AWS Management Console console.aws.amazon.com and find the
EventBridge at
All Services -> Application Integration -> Amazon EventBridge

Once we’re there, we click on
Events -> Rules
What we want to do is to create a rule which is based on a schedule.

The "Create Rule" screen requires the following input:
We enter a name and description of our choice.
The pattern is a “Schedule” and we define a fixed rate every 1 Minutes.
That makes testing easier, however, we need to make sure to change this setting afterwards

Next, we leave the default setting for event bus and we select our previously created Lambda function as “Target”.

No need to configure anything else, so we can go ahead and press “Create” at the bottom of the screen.

We can see that the rule is enabled and we can trust that it is doing its work.
4. Check results
This chapter is dedicated to those who don’t trust.
4.1. Check Amazon EventBridge
To check the execution of our scheduled rule, we go to the
Amazon EventBridge dashboard and click on our rule.
This takes us to the details screen of the rule, where we can see the "Monitoring" section.
Clicking the hyperlink takes us to the
CloudWatch Console
(direct access:
console.aws.amazon.com/cloudwatch)

4.2. Check AWS Lambda
It makes sense to revisit our Lambda function.
So we go to
console.aws.amazon.com/lambda and choose our function.
On the details screen, we can see that the “Designer” has added our
EventBridge rule, as a trigger, to the diagram.

Nice, but we wanted to view the “Monitoring” tab.
We can scroll down to the recent invocations, where we can see that the function indeed has been invoked every minute:

4.3. SAP BTP
Last check for today is to view the logs of our target application on the
SAP Business Technology Platform.
The logs can be found in the application details screen of the cloud cockpit, or on command line with the following command:
cf logs sappapp --recent
As a result, we get many log entries like this one:

So finally we can trust the setup of our scenario.
We’ve seen that we can define a schedule-event on
EventBridge that triggers our lambda which calls our app.
4.4. Cleanup
To avoid unnecessary cost, we should stop the minute-by-minute-execution.
We can change or disable or delete the rule on
Amazon EventBridge.
So we go to the
EventBridge console at
console.aws.amazon.com/events
There we can select our rule and e.g. click the “Disable” Button.
5. Optional: Homework
What we don’t cover in this blog post: how to react upon failures.
For instance, our Lambda calls our endpoint which fails to do its task.
Which in turn, causes our Lambda to fail.
In such case, an AWS health check could be created to raise an alert.
For instance
CloudWatch Lambda Insights:
"CloudWatch Lambda Insights is a monitoring and troubleshooting solution... collects, aggregates, and summarizes diagnostic information..."
Or anything similar.
Your homework.
Please, once you’re done, raise your finger and share your result with us (in the comment section) – thanks !
Summary
In this blog post we started with the requirement:
We want to schedule a job that calls a secured endpoint on SAP BTP.
That job should run on AWS.
We created the app on
SAP BTP.
We created an
AWS Lambda Function which calls that app.
We defined an
EventBridge schedule to trigger the function regularly.
Links
SAP BTP
AWS Lambda
Amazon EventBridge
Amazon Cloud Watch
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…
Appendix: Sample Application Code
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, to follow the tutorial it is enough to deploy the files, so you don't need to install Node.js.
xs-security.json
{
"xsappname" : "xsuaa_sappapp",
"tenant-mode" : "dedicated",
"scopes": [{
"name": "$XSAPPNAME.sappappscopp"
}],
"authorities":["$XSAPPNAME.sappappscopp"]
}
manifest.yml
---
applications:
- name: sappapp
path: .
memory: 128M
buildpacks:
- nodejs_buildpack
services:
- xsuaa_sappapp
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)
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...');
const authHeader = req.headers.authorization;
if (authHeader){
const theJwtToken = authHeader.substring(7);
if(theJwtToken){
console.log('===> [LOGGER] the received JWT token: ' + theJwtToken )
const jwtBase64Encoded = theJwtToken.split('.')[1];
if(jwtBase64Encoded){
const jwtDecoded = Buffer.from(jwtBase64Encoded, 'base64').toString('ascii');
if(jwtDecoded){
const jwtDecodedJson = JSON.parse(jwtDecoded);
console.log('===> [LOGGER]: JWT: scopes: ' + jwtDecodedJson.scope);
console.log('===> [LOGGER]: JWT: client_id: ' + jwtDecodedJson.client_id);
console.log('===> [LOGGER]: JWT: audience: ' + jwtDecodedJson.aud);
}
}
}
}else{
console.log('===> no authorization header')
}
next()
}
app.use(jwtLogger)
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 at ${new Date()} by ${req.headers['user-agent']}`)
const MY_SCOPE = xsuaaCredentials.xsappname + '.sappappscopp' // copied from xs-security.json
if(req.authInfo.checkScope(MY_SCOPE)){
res.send(`The protected endpoint was properly called, the required scope has been found in JWT token`);
}else{
return res.status(403).json({
error: 'Unauthorized',
message: 'The endpoint was called by user who does not have the required scope: <sappappscopp> ',
});
}
});
const port = process.env.PORT || 3000;
app.listen(port, function(){})
Appendix: Sample Lambda Function Code
Once the Lambda function is created, the content of the generated index.js file can be replaced completely by the following file.
index.js
const https = require('https');
const ENDPOINT_HOST = 'https://sappapp.cfapps.eu10.hana.ondemand.com'
const ENDPOINT_PATH = '/prot'
const OA_URL = 'https://<acc>.authentication.eu10.hana.ondemand.com'
const CLIENT_ID = 'sb-xsuaa_sappapp!t11111'
const CLIENT_SECRET = 'yoursecret'
exports.handler = async (event) => {
const jwtToken = await _fetchJwtToken()
const result = await _callSapEndpoint(jwtToken)
return `Function called SAP app which responded: [status ${result.status}] '${result.message}'`
};
const _fetchJwtToken = async function() {
return new Promise ((resolve, reject) => {
const options = {
host: OA_URL.replace('https://', ''),
path: '/oauth/token?grant_type=client_credentials&response_type=token',
headers: {
Authorization: "Basic " + Buffer.from(CLIENT_ID + ':' + CLIENT_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) => {
console.log("Error: " + error.message);
return reject({error: error})
});
})
}
const _callSapEndpoint = async function(jwtToken){
return new Promise((resolve, reject) => {
const options = {
host: ENDPOINT_HOST.replace('https://', ''),
path: ENDPOINT_PATH,
headers: {
Authorization: 'Bearer ' + jwtToken
}
}
https.get(options, res => {
res.setEncoding('utf8')
let response = ''
res.on('data', chunk => {
response += chunk
})
res.on('end', () => {
resolve({message: response, status: res.statusCode})
})
})
.on("error", (error) => {
reject({error: error})
});
})
}