Skip to Content
Technical Articles
Author's profile photo Nicolai Geburek

UI5 Apps and API Keys – A Secure Way

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.

Assigned Tags

      12 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Marcel Schork
      Marcel Schork

      Hi Nico,

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

      Author's profile photo Nicolai Geburek
      Nicolai Geburek
      Blog Post Author

      Hi Marcel,

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

      BR, Nico

      Author's profile photo Nicolai Geburek
      Nicolai Geburek
      Blog Post Author

      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

       

      Author's profile photo Marcel Schork
      Marcel Schork

      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...

      Author's profile photo UDAY SHANKAR M S
      UDAY SHANKAR M S

      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

      Author's profile photo Nicolai Geburek
      Nicolai Geburek
      Blog Post Author

      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

      Author's profile photo UDAY SHANKAR M S
      UDAY SHANKAR M S

      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

      Author's profile photo Nicolai Geburek
      Nicolai Geburek
      Blog Post Author

      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

      Author's profile photo UDAY SHANKAR M S
      UDAY SHANKAR M S

      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

      Author's profile photo Nicolai Geburek
      Nicolai Geburek
      Blog Post Author

      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

      Author's profile photo Kevin Hu
      Kevin Hu

      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

      Author's profile photo Nicolai Geburek
      Nicolai Geburek
      Blog Post Author

      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