
Often 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
"@sap-cloud-sdk/http-client": "^3.9.0",
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)
... 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: liteHealth 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.
"express": "^4.17.3", "body-parser": "^1.20.2", "passport": "^0.6.0", "passport-http": "^0.3.0"
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'); });
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)
{ "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" ] } }
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.AccessThe 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.
"@sap/xsenv": "^4.2.0", "@sap/xssec": "^3.6.0",(Use at least version 3.6.0 of xssec because of mentioned security issue)
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 } )
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
11 | |
10 | |
9 | |
8 | |
6 | |
6 | |
5 | |
5 | |
5 | |
5 |