Oftern there is a need for Apps running outside of the corporate network to access SAP onpremise systems. Using a Proxy App running in the BTP (Business Technology Platform) Cloud Foundry Environment utilizing the SAP Cloud Connector is a well known pattern for such requirements.
However using the destination and connectivity service to get access to the tunnel to the onpremise systems is not trivial and is best done with the help of libraries.The approuter (cf. https://www.npmjs.com/package/@sap/approuter) is a well known and proven solution for this. However approuter is aimed at web applications. Using it for eg native applicatios is a bit troublesome.
Since March 2019 there is also the SAP Cloud SDK for JavaScript (in the beginning known as the SAP S/4HANA Cloud SDK for JavaScript and also going by SAP Cloud SDK for Node.js) available. The http-client of this SDK completly handles the interaction with the destination and connectivity service, you just have to provide the destination name (please check out other features of this SDK, there is a lot of other useful stuff there!). This blog intends to give you some guidance for using this SDK for this porpose.
The SDK is availabe in the public npm repository. Add the line
In your JavaScript file to be run add
const httpclient = require('@sap-cloud-sdk/http-client');
The object thus obtained follows the axios HTTP client API with an added first object parameter (second parameter is compatible to RawAxiosRequestConfig), eg to make a POST call to an onpremise destination use this code
await httpclient.executeHttpRequest(
{
destinationName: 'MYDESTINATION'
},
{
method: 'POST',
url: "/sap/opu/odata/sap/ZMY_SERV_SRV/MyEntitySet",
headers: {
"Content-Type":"application/json; charset=utf-8",
"Accept":"application/json"
},
data: body
}
).then(response => {
// use eg response.status and response.data
}).catch(err => {
// use eg err.code or message
});
(Note: All code examples in this blog are just examples, do not use in your productive code, eg consider adding logging and a more robust coding)
CSRF handling is done by default.
For a first simple proxy example we will use Basic Authentication from Outside and also Basic Authentication to the onpremise system. The latter is an easy brainer: Just use Basic Authentication as Authentication Type when defining the destination in the BTP cockpit:
In the mta.yaml define the depencies to the destination and connectivity service:
...
modules:
- name: myproxy
type: nodejs
requires:
- name: myproxy-destination-service
parameters:
content-target: true
- name: myproxy-connectivity-service
parameters:
health-check-type: process
...
resources:
- name: myproxy-destination-service
type: org.cloudfoundry.managed-service
parameters:
config:
version: 1.0.0
service: destination
service-name: myproxy-destination-service
service-plan: lite
- name: myproxy-connectivity-service
type: org.cloudfoundry.managed-service
parameters:
service: connectivity
service-plan: lite
Health check is set to process because otherwise health check uses an unauthenticated HTTP access to / which won’t work with our basic authentication requirement and thus would give constant app restarts.
For the proxy function we use express and passport, ie in the package.json
"express": "^4.17.3",
"body-parser": "^1.20.2",
"passport": "^0.6.0",
"passport-http": "^0.3.0"
Preperation in the JavaScript file
const express = require('express');
const bodyParser = require('body-parser')
const passport = require('passport');
const passportHTTP = require('passport-http');
const auth_env = {login: process.env['AUTH_LOGIN'], password: process.env['AUTH_PASSWORD']};
const app = express();
passport.use(new passportHTTP.BasicStrategy(
function(username, password, done) {
if (username === auth_env.login && password === auth_env.password) {
return done(null, username);
} else {
return done(null, false);
}
}
));
app.use(passport.initialize());
app.use(passport.authenticate('basic', { session: false }));
app.use(bodyParser.text({ type: 'application/json' }));
...
app.listen(process.env.PORT || 5000, function () {
console.log('Proxy app started');
});
and code for proxying a POST call
app.post('/MyEntitySet', async (req, res) => {
if (!req.user) { res.sendStatus(403); return; }
await httpclient.executeHttpRequest(
{
destinationName: 'MY_DESTINATION'
},
{
method: 'POST',
url: "/sap/opu/odata/sap/ZMY_SERV_SRV/MyEntitySet",
headers: {
"Content-Type":"application/json; charset=utf-8",
"Accept":"application/json"
},
data: req.body
}
).then(response => {
res.send(response.data);
}).catch(err => {
res.status(500).send('Backend Error');
});
});
(in real life you might want to use wildcards and parse req.url)
Principal Propagation means the client needs to authenticate the user aka principal against the BTP and obtain tokens which need to be stored securely. When accessing the proxy the token needs to be presented. The token is then forwared to the cloud connector which generates authorization info using the principal for the onpremise system (configuring cloud connector and onpremise systems to achieve this is beyond the scope of this blog).
In this example the native client needs to obtain a JWT token and send it along with request to the proxy. The JWT token will eg be obtained and renewed by utilizing SAML 2.0 and OAuth 2 protocols with the XSUAA service, cf. https://sap.github.io/cloud-sdk/docs/java/guides/cloud-foundry-xsuaa-service . The native client should use libraries for this tasks (this is however beyond the scope of this blog). After deploying the proxy app to the BTP go to the deployed app in the BTP cockpit, select “Service Bindings” on the left and click on “Show sensitive data” to obtain the data for the libraries. You need
- token_service_url : Add /oauth/authorize for the SAML 2.0 login endpoint, /oauth/token for the OAuth 2.0 endpoint and /logout.do for the SAML 2.0 logout endpoint
- clientid and clientsecret for the OAuth 2 protocol
Also go to “Scopes” on the left and use the displayed scope name in the OAuth 2 protocol.
The configuration for xsuaa in the xs-security.json file is needed (add scopes and role-templates as needed):
{
"scopes": [
{
"name": "$XSAPPNAME.access",
"description": "Access"
}
],
"role-templates": [
{
"name": "Access",
"default-role-name": "My Proxy Access Authorization",
"scope-references": [
"$XSAPPNAME.access"
]
}
],
"oauth2-configuration": {
"redirect-uris": [
"http://localhost:9999/success"
]
}
}
Use Redirect-URIs as needed by the native libraries (can also use different schemes than http oder https as commonly used on mobile OS).
In the mta.yaml there is now a depedency to the xsuaa service (add role-collections as needed):
requires:
- name: my_proxy-uaa
...
resources:
- name: my_proxy-uaa
type: org.cloudfoundry.managed-service
parameters:
path: ./xs-security.json
service-plan: application
service: xsuaa
config:
xsappname: my_proxy-${space}
tenant-mode: dedicated
role-collections:
- name: 'my_proxy-Access-${space}'
role-template-references:
- $XSAPPNAME.Access
The xsappname and the role collections name thus contains the space name in order to be able to deploy the proxy app to multiple spaces in the same subacount (eg for three-tier development, test, production spaces). In this case the destination need to be defined at app-level in the respective destination service instance instead of subaccount level to have identical named destinations pointing to development, test, production onpremise systems respectivly (destination can’t be defined at space level). Assign the role collection to users, either individually or via groups or via IdP configuration.
The destination is defined with Principal Propagation:
In the package.json we need now additional dependencies to xsenv and xssec:
"@sap/xsenv": "^4.2.0",
"@sap/xssec": "^3.6.0",
(Use at least version 3.6.0 of xssec because of mentioned security issue)
In the JavaScript file the preperation now includes verification of the JWT token with the xsuaa service:
const httpclient = require('@sap-cloud-sdk/http-client');
const express = require('express');
const bodyParser = require('body-parser')
const xsenv = require('@sap/xsenv');
const passport = require('passport');
const xssec = require('@sap/xssec');
xsenv.loadEnv();
const app = express();
const services = xsenv.getServices({ uaa: 'my_proxy-uaa' });
passport.use(new xssec.JWTStrategy(services.uaa));
app.use(passport.initialize());
app.use(passport.authenticate('JWT', { session: false }));
app.use(bodyParser.text({ type: 'application/json' }));
The code for proxying a call now contains code for checking the scope and for fowarding the JWT token for principal propagation:
app.post('/MyEntitySet', async (req, res) => {
if (!req.authInfo.checkLocalScope('access')) {
return res.status(403).send('Forbidden');
}
await httpclient.executeHttpRequest(
{
destinationName: 'MY_DESTINATION'
jwt: req.authInfo.getAppToken()
},
{
method: 'POST',
url: "/sap/opu/odata/sap/ZMY_SERV_SRV/MyEntitySet",
headers: {
"Content-Type":"application/json; charset=utf-8",
"Accept":"application/json"
},
data: req.body
}
)
The SAP Cloud SDK for JavaScript makes it easy to use onpremise destiation with principal propagation in a BTP proxy and hides the complexity in dealing with the destination, connectivity and xsuaa service.
Be the first to comment