Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
nicoschoenteich
Developer Advocate
Developer Advocate
4,201
EDIT (January 2023)

Since writing this blog post in March 2022 I was made aware of an even simpler way of securely calling an API using an API key than the way I initially described in this blog post (see Gregor Wolf's comment). This is it:

When defining a destination via the SAP BTP Cockpit or a configuration file, these additional properties can be used to pass the API key:

URL.headers.HEADER_KEY

URL.headers.QUERY_KEY

This is described in the SAP Cloud SDK documentation.

I will keep the old blog post below for reference.

 




 

In this blog post I will describe how you can call an API that requires an API key from a UI5 app - without exposing the key to the client, which would be a severe security flaw.

 

I recently came across an API I wanted to consume that required an API key as a query parameter attached to the URL of the call. This is what the API expects:
https://www.domain.com/api?key=my-actual-api-key

 

The architecture of the app


In this scenario, the UI5 app is bound to an instance of the destination service in Cloud Foundry and runs with a standalone node.js based approuter (learn more about standalone and managed approuters here). To avoid any CORS issues, the app doesn't call the actual domain of the API, but calls a route (/myDestination) that is defined in the xs-app.json of the approuter. The approuter proxies all request that hit that route to the destination which is defined in the Cloud Foundry Environment (including the actual domain of the API).

What we should not do


Let's start with how we shouldn't include the API key in our app. The easiest thing to do would be to simply attach it to the uri of the dataSource in the manifest.json application descriptor:
"sap.app": {
"dataSources": {
"myAPI": {
"uri": "/myDestination/api?key=my-actual-api-key",
"type": "JSON"
},
...
}
},
...

If we did that the key would be visible to the client (anyone opening the app in their browser) through the Sources and Network tab of the Developer Tools. This would be a severe security flaw, as anyone could copy the key and do all kinds of things with it. We definitely want to avoid that. Another issue with this technique is that it is very likely we accidentally push this key to GitHub. The manifest.json is a very important file for UI5 projects and we cannot simply put it in the .gitignore file.

So what other options do we have? Unlike OAuth or other authentication methods the destination service in Cloud Foundry unfortunately doesn't support API keys or query parameters attached to the URL. This means we have to implement something custom, and it has to be server side, so it is not visible to the client.

 

What we will do


We will extend the approuter with a custom middleware (see the official documentation for more info), which will handle the API call on its own, instead of using the destination service. By writing a custom middleware we have full control over what the approuter does once our front end app hits a specific route. The approuter runs server side and it's code is not visible to the client, which is why it's a safe place to attach the API key to the request URL.

In our approuter folder, we create a new middleware.js file that imports the approuter package and starts the approuter manually:
const approuter = require('@sap/approuter');
const ar = approuter();

ar.start();

We modify the start script of the approuter in its package.json file so that it points to our new middleware.js file JavaScript file:
{
"name": "approuter",
"dependencies": {
"@sap/approuter": "^10"
},
"scripts": {
"start": "node middleware.js"
}
}

At this point, not a lot has changed. In fact, the behaviour of the approuter has not changed at all. It is just instantiated and started from a different place. But we can use the middleware.js file to define a beforeRequestHandler that will execute code just after a route is hit and before the request is proxied. We will then execute a new call using node-fetch, so let's install this package. Make sure to install version 2, as version 3 is not compatible with CommonJS modules (which we are using in this example):
npm install node-fetch@2

Next, we can create a new file called default-env.json (if it doesn't exist yet) and store our API key in there. This way it will be accessible to the middleware.js file as a node.js environment variable. The benefit of this is that we can later put the default-env.json file in our .gitignore file to avoid releasing the key to GitHub:
{
"MY_API_KEY": "my-actual-api-key"
}

Let's put all the pieces together. In our middleware.js we define the beforeRequestHandler, which replaces the /myDestination route with the actual domain and the keyword MY_API_KEY in the URL with actual API key (stored as node.js environment variable). It then fetches the data from the API, and sends the json response back to the client. The call is ended without proxying the request to a destination.
ar.beforeRequestHandler.use("/myDestination", async function myMiddleware(req, res, next) {
let newUrl =
req.url.replace("/myDestination", "https://domain.com")
.replace("MY_API_KEY", process.env.MY_API_KEY)
const response = await fetch(newUrl);
const data = await response.json();
res.end(JSON.stringify(data))
});


To trigger this handler and make it work properly we have to define the following uri as the dataSource in our manifest.json of our UI5 app:
"sap.app": {
"dataSources": {
"myAPI": {
"uri": "/myDestination/api?key=MY_API_KEY",
"type": "JSON"
},
...
}
},
...

 

And there we go, our UI5 app can now call the API (well, it calls the approuter, which then calls the API on our behalf) without even knowing the API key. And if the app doesn't even know the key, there is also no way for a hacker to find it 😉

 

Limitations


This simple solution comes with a few limitations. For now it only works for GET requests, as we haven't put much thought into supporting other request methods in our beforeRequestHandler function. This is definitely doable though.

Also, one of the downsides of this custom solution is that it is... well, custom. This means you as the developer of the app are responsible for the code and any updates/ changes that might be necessary in the future. This is especially true for anything related to security. It's usually easier (more secure) to rely on SAP's built in solutions such as the destination service. On a related note, I currently don't see any way of implementing this solution or something similar with a managed approuter.

 

Please share your thoughts and feedback.
16 Comments