Skip to Content
Technical Articles

SAP Cloud Platform: How to call – onPrem System – from Node.js app – via Cloud Connector

You have a Node.js application running in SAP Cloud Platform.
You want to call an API located on your onPremise system (e.g. S4, etc)
You want a sample based on Node.js
—> To get started, you might find it useful to go through this tutorial

Quicklinks:
Quick Guide
Featured Chapter
Sample Code

The scenario:

Prerequisites

  • Access to SAP Cloud Platform, some basic knowledge,
    e.g. how to deploy applications, using CLI
  • Node.js installed on local machine + some knowledge
  • Back-end is not required: we simulate it with local server on local laptop
  • Access to local laptop

Overview

  • Preparation on local laptop:
    create app to simulate back-end
    install Cloud Connector
  • Preparation in Cloud
    – create instance of destination service and configure destination
    – create instance of connectivity service
  • Write Node.js application
    create and implement app

Preparation 0: The Local Project

In our tutorial, we’re going to use 3 working directories:
– Local node.js project
– Local installation of SAP Cloud Connector
– Node.js project for app to deploy to cloud

Create root folder for all 3 working directories: C:\tmp_onPrem

Preparation 1: The Back-end Mock

To be able to follow this tutorial, we need an onPrem back-end
With other words, we need a server with an endpoint which is not in the cloud

This can be easily simulated with a mock server, running locally on our laptop

So let us quickly create that mock server, we just need to write a little Node.js app which exposes a dummy endpoint

1. Create mock server

Create working directory C:\tmp_onPrem\backendmock,
create files like shown in Appendix 1
copy the file contents

On command line, jump into the folder backendmock and execute npm install
This will install the express module

2. Start mock server

On command line, jump into folder backendmock and execute node server.js
This will start up the server

3. Run local endpoint

Open a browser of your choice and call the following address:
http://localhost:8080/products

This is the port and endpoint as written in our back-end-mock-server-app
The result is disgusting: we get an error on our own local app…!
But this is not surprising, because we’ve added a little silly authentication strategy to our code:

const auth = req.headers.authorization || ''
const user = Buffer.from(auth.substring(6), "base64").toString('utf8') 
if(user === 'john:secret'){ 
. . .

Yes, it is silly code, because we don’t use passport middleware, etc –yeeees— but this way is simpler and I don’t mind if a silly back-end has silly source code

Now open a REST client of your choice and enter user credentials (of the only one permitted user) as Basic Auth:
user: john
password: secret

Then invoke the endpoint with the REST client and check the response
As a result, you can see the list of products.
A silly small list, what a shame

Finally, now we have an onPremise back-end
And it is (somehow) “protected'” with basic auth
…I know it makes you laugh…;-)

Preparation 2: SAP Cloud Connector

Please forgive that we cannot go in detail through the required steps.
Everything is well explained in the official SAP Help Portal
The focus of this tutorial is in the implementation of the cloud app.
Nevertheless, I’ve taken some screenshots and pasted them in Appendix 3

1. Install SAP Cloud Connector

To download the portable zip, go to
https://tools.hana.ondemand.com/#cloud
scroll down to “Cloud Connector”
and choose the installation package.
In my case it was this link:
https://tools.hana.ondemand.com/additional/sapcc-2.12.4-windows-x64.zip

It can be extracted to any folder of your choice.
In my example I’ve created a subfolder of our project root:
C:\tmp_onPrem\CloudConn
And extracted the zip there. Nothing else to do for installation

2. Start Cloud Connector

To start the Cloud Connector, open command prompt and jump to the installation folder.
Execute go.bat and wait for final log statement

Then we can open the following URL in our browser:
https://localhost:8443/
Initial credentials: Administrator / manage

3. Configure Cloud Connector

First thing to do is some very basic config:
Cloud Conn wants us to change the password, so we change it
We leave the installation type as “master”
We press “Save” in the upper corner

Now we start with the real configuration.
Don’t worry, for our simple scenario it isn’t complicated

3.1. Plug the Cloud

Cloud Connector is used to connect onPremise systems to the cloud
We see: 2 parties to be connected. Both need to be plugged into Cloud Connector

First, let’s specify the “cloud”-side a little bit more concrete

What we need to enter is some info of the Subaccount into which we want to plug

Region
In my case, I’ve chosen eu10

Subaccount
This is not the name of the Subaccount. Instead, we have to search for the ID
We can find the Subaccount ID in the cockpit, on the overview page of the subaccount

Display Name
This can be the name of the subaccount. The value entered here will appear in the Cloud Connector UI

Login E-Mail + Password
The credentials of the cloud platform user

Location ID
This is only necessary, if there are multiple Cloud Connectors connected to the same subaccount
Let’s enter a value here, to make it little bit more complicated
Also, it allows us to not get in trouble which our admin, who configured a real professionally used cloud connector for our subaccount

After saving the configuration, we get an info message which is related to the locationID and which we can ignore

Now we can check if our Cloud Connector has been recognized by the cloud platform
We go to the cloud cockpit, then “Subaccount-> Connectivity->Cloud Connectors”
Here our local Cloud Connector should be displayed

3.2. Plug the onPrem

Now that the Cloud Connector is connected to the cloud, we can take care of the other side:
the onPrem side.
In our case, we want to plug our local mock server into Cloud Connector

In Cloud Connector UI, we click on “Cloud To On-Premise” on the left menu
We press the + icon to create a new “Mapping Virtual to Internal System”

Back-end Type
As Back-end Type we choose “Non-SAP-System”

HTTP
yes, “HTTP”

Internal Host
Next, we have to enter the “internal host”. This is the “real” host, the real back-end system.
The one which we can touch with our browser, the one which we can enter in the browser Address bar:
Internal Host: localhost
Internal Port: 8080

Virtual Host
This one is used to hide the real host name. So let’s hide it and no colleague who looks at the cloud cockpit, can assume that our silly local cloud connector is used and that the connected onPrem system a “fake”
Virtual Host: s4hmock
Virtual Port: 3000

Principal Type
None

Host In Request Header
Use Virtual Host

Description
<empty>

After finishing the wizard, we have to do one last configuration:
which Resources are allowed to be accessed (and forwarded) by the cloud connector?

We press the + icon of the “Resources” section and specify:

URL path
We enter just a slash: /
This means that all resources on our server can be exposed

Access Policy
Here we select: “Path and all Sub-Paths”

Finally, we can check the connection by pressing the “Check availability” icon (see screenshot in appendix)

So now our dumb mock server is plugged into the Cloud Connector

At the end of this preparation section, we can again have a look at the cloud cockpit
Now our local mock server has been added in section “Exposed Back-End Systems” and is marked as green, saying that resources are available

Don’t you agree that this is cool:
Our silly mock server is prominently appearing, visible for everybody, in the subaccount

3.3. Recap
We’ve connected our Cloud Connector to the cloud and to our local server
Now we can go ahead and start with the tutorial

Preparation 3a: Destination Service

The tutorial doesn’t really start here: this is another preparation section

It is a recommended and common and best practice to use destinations when calling URLs from any application in the Cloud Platform

1. Create instance of Destination Service

If not already done, we need to create an instance of destination service
In my example, I’ve called it destinationinstance

2. Create Destination Configuration

Now we can create a destination configuration.
Here we enter the URL of the target that we want to call, along with credentials of a (technical) user who has permission to consume the back-end service URL
(Here it would be a good option to use Principal Propagation. But that’s a different topic and we don’t need it in our scenario)

We have to pay attention how to configure the destination:

Name
Here we don’t need much attention. This is a name of our choice.
The configuration has a name and we’ll need the name later, in order to retrieve it

TYPE
The type of destination is HTTP

Description
We can leave it empty

URL
Here we don’t enter the URL of the back-end service. This is important.
The back-end system is not reachable from the cloud
That’s why we configured the Cloud Connector and that’s why we have a virtual system exposed by the Cloud Connector
Remember it?
So that’s what we have to enter here: the virtual URL
It makes sense to enter only the host, not a full URL, because that way we’re more flexible if we want to call multiple endpoints of the back-end
So finally we enter the values which we copy from our Cloud Connector Admin UI:
http://s4hmock:3000

Proxy Type
Here we have to choose onPremise. The other option wouldn’t require a Cloud Connector

Authentication
Remember that in our back-end service mock, we did little silly simulation of a basic authentication? There’s a little check for an authenticated user in the code. That hard-coded user is the one which we have to hard-code here again.
As such, we choose Basic Authentication and enter the
user name as john and the
password as secret
Note: it is a valid scenario to create a user in the back-end which is not a real user, but which has only very restricted roles, required to call one API. That’s a so-called “Technical User”, which can be used to call the back-end API. This is not useful for all scenarios, because often we want that the end-user who logs into the cloud, would also be forwarded to the back-end. But in our tutorial, there’s no data specific for users, so a technical user is fine

Location ID
This is the place to enter the location id which we configured in the Cloud Connector: mylocalmachine
If there are multiple Cloud Connectors connected to the subaccount, this is the way how the correct one is identified

That’s it.
We can save the destination configuration
There’s a little helper button  which tries to check the connection
The “Check Connection” will mostly be green. But note that this isn’t a “real” end-to-end check.
The check fails if the Cloud Connector or the back-end are offline. Also it fails if the virtual host would be deleted in Cloud Connecto
But it would still be green if the endpoint would be broken, user invalid etc.
It is just a connection check
And there’s no better check.

Note:
We cannot invoke http://s4hmock:3000/products
Why not?
Hey, we know that it is only a “virtual” host, it exists only in our imagination
It only becomes reality in conjunction with a proxy server
How to get that proxy server?
It is provided by the connectivity service
So that’s our next step

Preparation 3b: Connectivity Service

Still a preparation step – but important
However, it is easy to remember:
Whenever we want to call an onPrem system, we need the Connectivity service
Because onPrem needs to be connected

It is like plugging a laptop to the cloud
The Cloud Connector is the cable
The Connectivity Service is the plug socket
OK?
(Silly comparison…)

1. Create instance

Again, this step is easy. Just create an instance of connectivity service
No params etc required
No configuration required

2. Use it

This step requires more effort.
Using the Connectivity Service means to call the proxy server programmatically
That’s not a preparation step
That’s the ACTUAL content of this tutorial
Which title did I choose for this tutorial?
It must have been meaningful like:
“How to use Connectivity Service”
Or
“How to use Connectivity Service in node.js application”
Any other title would have been silly

OK, now let’s do it.

Step 1: Create Node.js App

The tutorial finally starts with step 1.
We want to create a Node.js app which calls the silly endpoint of our dumb mock server to fetch useless data.
This app is meant to run in the cloud, use the destination configuration, then call the virtual host on Cloud Connector, which finally forwards the request to our mock server

But there’s a piece missing in the picture

The missing piece:
How to call the virtual host?
As we realized: it is not real

And here comes the Connectivity Service with its proxy server
Now the scenario is complete

This has been an overview
Not a real “step 1”
Must have been just another fake..

Step 1: Create Node.js App

Now we can start the tutorial
Really.

We create a small standard node app inside a new working directory below our root project folder
C:\tmp_onPrem\cloudToOnprem
See the folder structure marked in the screenshot below

The app folder contains only the minimum files required to deploy a node app to the Cloud Foundry environment of SAP Cloud Platform
The full content of the files can be found in the Appendix 2

Step 2: Implement App           <———-   Featured  ;-))    👍

1. manifest.yml

In the manifest file we declare the usage of the instances of destination service and connectivity service which we created above

  services:
  - connectivityinstance
  - destinationinstance

2. package.json

Here we declare dependencies to express (to provide a server with an endpoint) and axios (to execute requests)

    "dependencies": {
        "express": "^4.16.3",
        "axios": "^0.18.0"

On command line, jump into the folder cloudToOnprem and execute npm install

3. server.js

Our implementation file for all the code. Of course, you would structure it in a better way, but for a blog, where I cannot upload project, it is better to keep the number of files to be shown in the appendix rather low

One basic thing we do in the beginning of our code is to access the environment variable of our app to get the credentials of the destination and connectivity service
This is possible because we’ve bound our app to these instances

const VCAP_SERVICES = JSON.parse(process.env.VCAP_SERVICES);
const destSrvCred = VCAP_SERVICES.destination[0].credentials;
const conSrvCred = VCAP_SERVICES.connectivity[0].credentials;

We need these credentials in order to call the service instances

4. Define app endpoint

We need an express server and an endpoint, such that we can interact with our app in the cloud

const express = require('express')
const app = express()
. . .

app.listen(process.env.PORT, function () {
. . .

app.get('/callonprem', async function (req, res) {
. . .

5. Call Destination Service

We want to call the destination service in order to access the destination configuration which we created in the preparation section above
We know that this step is optional, as we could as well hardcode the URL and the user credentials here in the code
But as mentioned, using the destination service is better

The destination service itself is protected with OAuth 2.0 so we have to do the OAuth flow before we can read the configured destination

So first we have to fetch the JWT token, which we do in a helper function

const destJwtToken = await _fetchJwtToken(destSrvCred.url, destSrvCred.clientid, destSrvCred.clientsecret)

In the implementation of the helper function, we do a simple GET request with axios, which we configure with the required info

const _fetchJwtToken = async function(oauthUrl, oauthClient, oauthSecret) {
   const tokenUrl = oauthUrl + '/oauth/token?grant_type=client_credentials&response_type=token'  
   const config = {
      headers: {
         Authorization: "Basic " + Buffer.from(oauthClient + ':' + oauthSecret).toString("base64")
      }
   }
   axios.get(tokenUrl, config)
. . .

URL
The URL is composed by the oauth endpoint which we’ve read from the environment variable
The appended parameters are specific to the OAuth flow (more info here)
The OAuth endpoint is located on the OAuth Authorization Server, which is provided by XSUAA

Authorization
We have to provide credentials (basic auth) to access that server
Luckily, we have these credentials in the environment variable

Once we have the JWT token, we can call the destination service
The URL of the service is handed over to us in the environment variable as well
Furthermore, the REST API of the destination service allows to ask for one specific destination. As such, we pass the destination name which we entered in the destination configuration

So we can create a helper function which we invoke as follows:

const destiConfi = await _readDestinationConfig('destination_s4hmock', destSrvCred.uri, destJwtToken)

In our helper function we use the JWT token for authorization on the destination service
The URL is composed using the env variable and according to the docu of the REST API

const _readDestinationConfig = async function(destinationName, destUri, jwtToken){
   const destSrvUrl = destUri + '/destination-configuration/v1/destinations/' + destinationName  
   const config = {
      headers: {
         Authorization: 'Bearer ' + jwtToken
      }
   }
   axios.get(destSrvUrl, config)

 

The result of the call is a JSON object carrying all the configuration data which we entered when we created the destination configuration above
And yes, we get the password of the technical user in plain text
But you don’t need to panic, it is the same password which I wrote above

6. Call Connectivity service

Now that we know that URL that we want to call (we knew it anyways, we could have saved lot of “reading minutes” for this blog…), we can go ahead

But as mentioned before we cannot just call a “virtual” URL like
http://s4mock:3000/products

This service-URL is served by the Cloud Connector along with a proxy server
The URL of the proxy server is provided by the connectivity service
So let’s retrieve it

Again, to call the connectivity service, we have to authenticate with OAuth 2.0
Again, to fetch the token, we get the required credentials from the environment variable of  our deployed and bound application
So we can use our helper function in the same way like above

const connJwtToken = await _fetchJwtToken(conSrvCred.token_service_url, conSrvCred.clientid, conSrvCred.clientsecret)

And then we use the token to call the proxy server

The URL of the proxy server is taken from the env

connSrvCreds.onpremise_proxy_host
connSrvCreds.onpremise_proxy_http_port

The internal proxy looks like this in my example:

Here’s an example of how a proxy server looks in my case:
connectivityproxy.internal.cf.eu10.hana.ondemand.com

The authorization is done with the JWT token, as mentioned, but different than before:
There’s a special header required to authenticate at the proxy:

headers: {
   'Proxy-Authorization': 'Bearer ' + connJwtToken,

The actual target URL which we call, is our virtual host, as retrieved from the destination, concatenated with the path to the endpoint, as defined in our silly mock back-end

Another interesting point: since our Cloud Connector is identified via LocationID, we have to specify that somewhere.
This is the reason why we specified a LocaltionID at all: to be able to show how it is specified.
And here comes the solution:
It is another header: ‘SAP-Connectivity-SCC-Location_ID’
And we have the value in the destination configuration, we don’t have to copy&paste
It looks like this:

headers: {
   'SAP-Connectivity-SCC-Location_ID': destiConfi.CloudConnectorLocationId   

That’s all we need

We have obviously a helper function to do the work

const result =  await _callOnPrem(conSrvCred.onpremise_proxy_host, conSrvCred.onpremise_proxy_http_port, connJwtToken, destiConfi)

and the work is done like this:

const _callOnPrem = async function(connProxyHost, connProxyPort, connJwtToken, destiConfi){
   const targetUrl = destiConfi.URL + '/products' 
   const encodedUser = Buffer.from(destiConfi.User + ':' + destiConfi.Password).toString("base64")
    
   const config = {
      headers: {
         Authorization: "Basic " + encodedUser,
         'Proxy-Authorization': 'Bearer ' + connJwtToken,
         'SAP-Connectivity-SCC-Location_ID': destiConfi.CloudConnectorLocationId        
      },
      proxy: {
         host: connProxyHost,
         port: connProxyPort 
      }              
   }
   axios.get(targetUrl, config)

 

The result of this call is the response of our back-end-mock
We can do with it what we want.
We want nothing
So we just pass it as response or our cloud app

Please forgive me that there’s no error handling etc
(My apologies aren’t meant totally sincere, the blog is already too long, so skipping as many lines as possible)

Step 3: Deploy and Run

Finally we can push our app to the cloud
Once it is successfully deployed, we can invoke our test-endpoint.
In my example, the URL is as follows:
https://onpremcaller.cfapps.eu10.hana.ondemand.com/callonprem

And the result is the response of our local back-end mock server:

We can see:
Our app in the cloud has gone through the Cloud Connector and fetched the data exposed by the mock server on our local laptop

Cool – isn’t it?

Step 4: Undeploy and Sleep

We’ve reached the end of our tutorial (after 18 minutes of read…)
We can now clean the cloud and remove our dirty onPrem-Test-application

 cf d onpremcaller

We can also stop our mock server
And we can shutdown our Cloud Connector
And we can put our laptop in sleep mode
Good bye … 😴

Summarize

Instead of saving some lines and some seconds of read, I have to add this summary, because it is best practice, etc
So, as all of you noticed, we’ve managed to call onPrem system from the cloud
We’ve deployed an app to the cloud and this app was able to call an endpoint on our local laptop

That’s a bit surprising and it was made possible with Cloud Connector and Connectivity Service
Furthermore, we used the Destination Service (instead of saving some minutes of read)

The real scenario with details:

 

Quick Guide

The essential takeaway of this tutorial, from my perspective:

To call onPrem from Cloud Foundry, we need to call destination service and get proxy info
Then call the virtual URL via proxy:

const config = {
   headers: {
      Authorization: "Basic " + encodedUser,
      'Proxy-Authorization': 'Bearer ' + connJwtToken,
      'SAP-Connectivity-SCC-Location_ID': destiConfi.CloudConnectorLocationId        
   },
   proxy: {
      host: connProxyHost,
      port: connProxyPort 
   }              
}
axios.get(targetUrl, config)

Links


Appendix 0: Folder Structure

Root project and working directories:

Appendix 1: Sample Code of Mock Back-end

package.json

{
  "dependencies": {
    "express": "^4.16.3"
  }
}

server.js

const express = require('express');
const app = express()

app.get('/products', function(req, res){ 
   const auth = req.headers.authorization || ''
   const user = Buffer.from(auth.substring(6), "base64").toString('utf8') 
   console.log("User: " + user)
   if(user === 'john:secret'){ // simulating basic auth
      res.json([{Id: "HT-1000", name: "Broken Laptop"}]);
   }else{
      res.status(401).send('Forbidden: invalid user or password');   
   }
});

app.listen(8080, () => {
   console.log('server running')
})

Appendix 2: Sample Code of Node.js App

manifest.yml

---
applications:
- name: onpremcaller
  command: node server.js
  memory: 128M
  buildpacks:
  - nodejs_buildpack
  services:
  - connectivityinstance
  - destinationinstance

package.json

{
    "dependencies": {
        "express": "^4.16.3",
        "axios": "^0.18.0"
    }
}

server.js

const axios = require('axios')
const express = require('express')
const app = express()

const VCAP_SERVICES = JSON.parse(process.env.VCAP_SERVICES);
const destSrvCred = VCAP_SERVICES.destination[0].credentials;
const conSrvCred = VCAP_SERVICES.connectivity[0].credentials;

app.listen(process.env.PORT, function () {
    console.log('CloudToOnprem application started')
})

app.get('/callonprem', async function (req, res) {
    // call destination service
	const destJwtToken = await _fetchJwtToken(destSrvCred.url, destSrvCred.clientid, destSrvCred.clientsecret)
	const destiConfi = await _readDestinationConfig('destination_s4hmock', destSrvCred.uri, destJwtToken)

    // call onPrem system via connectivity service and Cloud Connector
	const connJwtToken = await _fetchJwtToken(conSrvCred.token_service_url, conSrvCred.clientid, conSrvCred.clientsecret)
	const result =  await _callOnPrem(conSrvCred.onpremise_proxy_host, conSrvCred.onpremise_proxy_http_port, connJwtToken, destiConfi)
    res.json(result)
})

const _fetchJwtToken = async function(oauthUrl, oauthClient, oauthSecret) {
	return new Promise ((resolve, reject) => {
		const tokenUrl = oauthUrl + '/oauth/token?grant_type=client_credentials&response_type=token'  
        const config = {
			headers: {
			   Authorization: "Basic " + Buffer.from(oauthClient + ':' + oauthSecret).toString("base64")
			}
        }
		axios.get(tokenUrl, config)
        .then(response => {
		   resolve(response.data.access_token)
        })
        .catch(error => {
		   reject(error)
        })
	})   
}

// Call Destination Service. Result will be an object with Destination Configuration info
const _readDestinationConfig = async function(destinationName, destUri, jwtToken){
	return new Promise((resolve, reject) => {
        const destSrvUrl = destUri + '/destination-configuration/v1/destinations/' + destinationName  
        const config = {
			headers: {
               Authorization: 'Bearer ' + jwtToken
			}
        }
		axios.get(destSrvUrl, config)
        .then(response => {
           resolve(response.data.destinationConfiguration)
        })
        .catch(error => {
	      reject(error)
        })
	})
}

const _callOnPrem = async function(connProxyHost, connProxyPort, connJwtToken, destiConfi){
    return new Promise((resolve, reject) => {
        const targetUrl = destiConfi.URL + '/products' 
        const encodedUser = Buffer.from(destiConfi.User + ':' + destiConfi.Password).toString("base64")
    
        const config = {
            headers: {
                Authorization: "Basic " + encodedUser,
                'Proxy-Authorization': 'Bearer ' + connJwtToken,
                'SAP-Connectivity-SCC-Location_ID': destiConfi.CloudConnectorLocationId        
            },
            proxy: {
				host: connProxyHost, 
				port: connProxyPort 
            }              
        }
		axios.get(targetUrl, config)
        .then(response => {
           resolve(response.data)
        })
        .catch(error => {
	      reject(error)
        })
	})    
}

 

Appendix 3: Screenshots of Cloud Connector

  • Project structure and working directory for unzipped Cloud Connector:

  • Start up of Cloud Connector: up and running:

  • Plugging the Cloud Connector to my subaccount

  • Cloud Cockpit: Subaccount: view our Cloud Connector appearing in the cloud

  • Plugging the Cloud Connector to my local mock server (representing the onPrem system)

  • Check the connection

  • Second configuration step for onPrem connection

 

 

 

 

2 Comments
You must be Logged on to comment or reply to a post.