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
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
marcel_schork
Explorer
0 Kudos

Hi Nico,

could the Credential Store be an option for both standalone and managed approuter?

nicoschoenteich
Developer Advocate
Developer Advocate
0 Kudos
Hi Marcel,

Don't have any experience with it yet, but will check it out (Y). Thanks for suggesting it.

BR, Nico
nicoschoenteich
Developer Advocate
Developer Advocate
0 Kudos
Hi Marcel,

I had a look at it.

The SAP Credential Store is a great option for storing credentials and you don't have to do it in a node environment variable. But still, the call that is made to get the credentials, whether it's an environment variable or an external call to (a destination pointing to) the instance of the Credential Service, needs to be performed by a server (not your front end application code - for security reason). That's why you need to be able to configure the server code, which is an option I currently don't see with the managed approuter.

I would love if someone proves me wrong.

BR, Nico

 
marcel_schork
Explorer
Hi Nico,

thanks for sharing the information. A colleague of mine is currently investigating if there is the possiblity to use the credential store directly from a frontend using the managed approuter approach. Will let know about the results here. For sure there must be a "secure place" inside your browser to store such information. Not sure if this is already possible nowadays...
UdayMS
Participant
0 Kudos
Hi Nicolai,

Great blog!

If I am sending some query parameters to the API using a API key in a AJAX call, how do I bundle that using the middleware server side code as mentioned in the blog. eg, I have a mta application with a ui5 module and I am capturing the input values as queries and sending to the API. Little confused as to what job will the controller of the UI5 application will do. Also where to create default-env.json file 

 

Thanks

Uday
nicoschoenteich
Developer Advocate
Developer Advocate
0 Kudos
Hi Uday,

Can you elaborate a little more on the architecture of your app? Are you running your UI5 app with an approuter or not? Do you want to send multiple query parameters to your API? What request methods do you need to support (I only cover "get" in this post here)?

Best Regards, Nico
UdayMS
Participant
0 Kudos
Hi Nicolai

We use MTA application with fiori module inside.Currently we sre using managed app router but can try standalone as well. Basically there are 2-3 input fields in view  which we capture and make an ajax call to rest api in controller ( which is basically cpi iflows exposed in apim). These parameters are sent in. GET call with api key in request header.

Thanks

Uday
nicoschoenteich
Developer Advocate
Developer Advocate
0 Kudos
Hi Uday,

I think it's worth testing the standalone approuter in this case (also see my discussion about this with Marcel above). Apart from that your set up sounds exactly like mine in the blog post so I think you are good to go. In your approuter middleware you want to extract all your query parameters from the header and url of the request that hits your defined route, replace the key placeholder with the actual key, and then send a new get request to your api.

Best Regards, Nico
UdayMS
Participant
0 Kudos
Hi Nico

Thanks for the inputs. How will api key for dev staging and prod be identified? Do we have to put them in default-env.json file for all landscapes and read based on some condition to identify environments? Also if you could share app structue or folder structure of full app it wii be helpful

Thanks

Uday
nicoschoenteich
Developer Advocate
Developer Advocate
0 Kudos

Hi Uday,

dev and prod environments technically wouldn't behave any different in the given scenario. But if you have different keys for different environments you could get the node environment variable and define a condition based on that.

The structure could look something like this:

approuter
webapp/ //build process move this here
default-env.json
middleware.json
package.json
xs-app.json
webapp
controller/
view/
Component.js
index.html
manifest.json
...
.gitignore

Best Regards, Nico

kevin_hu
Active Participant
0 Kudos
Would it be another option to use CAP, and consume the rest api and expose a local api? can utilize a lot of out of box features such as CRUD and dev/prod profile. more footprint of course.

It is still a mystery why destination service does not support URL query parameters. It seems some other product does.

https://answers.sap.com/questions/13530293/additional-parameter-sapquery-in-destination-has-n.html
nicoschoenteich
Developer Advocate
Developer Advocate
0 Kudos
Hi Kevin,

Yes, that sounds like it would work. Might even be a better option than what I showed in the blog post, as it would support all CRUD operations out of the box.

Thanks for sharing the link, too.

Best Regards, Nico
gregorw
Active Contributor
Hi Nico,

I think based on the SAP Cloud SDK documentation Additional Headers and Query Parameters on Destinations this can be solved directly in the destination with this Additional Parameters:

  • URL.headers.HEADER_KEY for headers

  • URL.queries.QUERY_KEY for query parameters


I've tested this with the ui5.yaml destination configuration and also deployed to the managed approuter in my sample ui5-document-translation.

Best Regards
Gregor
nicoschoenteich
Developer Advocate
Developer Advocate
0 Kudos
Hi Gregor,

very nice, I wasn't aware of that. Thanks for sharing. This kind of makes this blog post obsolete... I will test it myself and add a new section.

Best, Nico
Soumya
Active Participant
0 Kudos
Hi Nicolai and other experts,

My requirement is to call a API ( with APIKEY or OAuth2 tokens, yet to be decided ) from an UI5 application on ABAP repository (on-premise).

Please suggest the secure way of achieving without putting the values (APIKEY / Client Id / Client Secret) in manifest.json.

Thanks, Soumya
nicoschoenteich
Developer Advocate
Developer Advocate
0 Kudos
Hi there,

please check out the first paragraph of this blog post (titled "EDIT (January 2023)"). This is probably your way to go.

Best, Nico