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: 
rileyrainey
Product and Topic Expert
Product and Topic Expert
Overview: When creating a SAPUI5 Stand-alone applications for deployment on SAP Cloud Platform Cloud Foundry, you can employ an Approuter component to provide seamless authentication and authorization. This works fairly well. Still, there's a few tweaks that can be added to improve the overall user experience in such an application.

We recently built a stand-alone SAPUI5-based application for our own internal use. The application architecture is shown below.  The app is deployed in SAP Cloud Platform Cloud Foundry. It is composed of several microservices and we tie the whole thing together using an instance of the XSA Approuter.  The SAPUI5 static content is stored in and served up from the Approuter itself - more on that in a minute.



Approuter provides some nice features to integrate application security into your enterprise identity infrastructure.  As part of that, Approuter provides built-in session management and will redirect the user to the UAA service to authenticate whenever needed. It will also also manage OAuth2 security tokens on your user's behalf with only minimal extra programming on your part.  In short, using Approuter will save you a lot of time building integrated application security into a Cloud Foundry app.

We built and deployed the first version of our application early in 2019. We've been using it for over six months.  Our practical experience has been that the Approuter, for the most part, works as we'd expect. There's one annoying wrinkle though: If a user returns to the application's browser tab after a long period of inactivity, we do see some less than perfect user experience with out-of-the-box UI5.  The user will encounter unfriendly, developer-oriented error pop-ups.  If the user is savvy, pressing the browser refresh button clears things up.  But we can do better. Addressing that that is what I want focus on in this article.

Inactivity Logout in SAPUI5


The core issue that we're tackling here is to address what should happen when an Approuter session is expiring.

There's basically three ways we could handle this:

  1. Ensure the user's session never expires

  2. Catch and respond to the SAPUI5 errors that would occur when a session expires (this turns out to be a bit tricky; I hope to address this in a separate blog post)

  3. Log the user out (just before) the session expires (what we are doing in this article)


The core idea behind what we're adding is to intervene and log out before the Approuter session expires.

Configuring Approuter to Support Logout


Let me review a few configuration basics.  Approuter's behavior is controller by settings in two files: xs-app.json and the deployment manifest.yml.

Let's walk through the xs-app.json file. There are routes to all locations "behind" the Approuter application. You will need to insert patterns to represent all appropriate URLs within your application in that section.
{
"routes": [
{
"source": "^/odata.svc/(.*)$",
"destination": "tdash-odata-service",
"authenticationType": "xsuaa"
},
{
"source": "^/api/(.*)$",
"destination": "tdash-service",
"authenticationType": "xsuaa"
},
{
"source": "^/swagger.*",
"destination": "tdash-service-swagger",
"authenticationType": "xsuaa"
},
{
"source": "^/v2/.*",
"destination": "tdash-service-swagger",
"authenticationType": "xsuaa"
},
{
"source": "^/webjars/.*",
"destination": "tdash-service-swagger",
"authenticationType": "xsuaa"
},
{
"source": "^/logged-out.html$",
"localDir": "resources",
"authenticationType": "none"
},
{
"source": "^/(.*)$",
"localDir": "resources",
"authenticationType": "xsuaa"
}
],
"logout": {
"logoutEndpoint": "/logout",
"logoutPage": "/logged-out.html"
}
}

The application's xs-app.json file.


As you might expect, /odata.svc URL points to the microservice handling the OData API.  This microservice is designed to expect XSA-style OAuth2/JWT tokens for authorization -- those are generated and supplied on demand by Approuter. Routes that require such a token are marked with the "authenticationType": "xsuaa" configuration directive.  It turns out that "xsuaa" is the default, but I like to include it for clarity's sake.

It turns out that for mostly historical reasons, this application also supports a RESTful API. The next four routes define URLs associated with that REST API and its associated Swagger interface.

The last two route directives specify details of accessing the static SAPUI5 content. We chose to place the static content within the Approuter application itself -- at a component level, doing this separates the UI from web services without requiring the adding a component application specifically devoted to the UI.

I also added a logout section to this configuration file.  The logoutPage directive allows you to specify a static page to be displayed when a user has been explicitly logged-out from the application.  The logoutEndpoint directive specifies the path you'd like to associate with triggering a logout -- in short, invoke that URL and the user's session will be completely torn down and the browser will be redirected to the logout page.

The application's Cloud Foundry deployment manifest.yml looks like this:
applications:
# Application Router
- name: tdash
routes:
- route: tdash((DASH))((UNIQUE_ID)).((LANDSCAPE_APPS_DOMAIN))
path: src
buildpacks:
- nodejs_buildpack
memory: 128M
stack: cflinuxfs3
services:
- xsuaa
- logging

env:
SESSION_TIMEOUT: 120
destinations: >
[
{
"name":"tdash-service",
"url":"https://tdash-service((DASH))((UNIQUE_ID)).((LANDSCAPE_APPS_DOMAIN))",
"forwardAuthToken": true
},
{
"name":"tdash-service-swagger",
"url":"https://tdash-service((DASH))((UNIQUE_ID)).((LANDSCAPE_APPS_DOMAIN))"
},
{
"name":"tdash-odata-service",
"url":"https://tdash-odata-service((DASH))((UNIQUE_ID)).((LANDSCAPE_APPS_DOMAIN))",
"forwardAuthToken": true
}
]

The approuter's manifest.yml file


SESSION_TIMEOUT sets the inactivity timeout in minutes of a user's session for your application.  Specifically, this is the HTTP session between the user's browser and the Approuter application itself.  Conversations between the Approuter and the backend services will normally include supplying session-specific JWT/OAuth2 authorization tokens. That's managed automatically by Approuter.

In the current  version of Approuter, the default SESSION_TIMEOUT is 15 minutes.

The destinations map the destinations named in the xs-app.json file to the URL of each deployed microservice.

Sidebar: Using Symbolic Parameters in a Manifest file


One small diversion here: you are probably noticing several directives surrounded by double-parentheses in the manifest file. We're leveraging this feature to extract the elements of this file that will be different for deployments into production, QA, and development spaces.  These settings are defined in a separate YAML file and are substituted in via the command line:
$ cf push -f manifest.yml --vars-file ../qa-vars.yml

Our qa-vars.yml file looks like this:
# a path fragment to make the urls unique
UNIQUE_ID: "qa"

DASH: "-"

TDASH_RELEASE: 0.11.0.LATEST

# Choose cfapps.eu10.hana.ondemand.com for the EU10 landscape, cfapps.us10.hana.ondemand.com for US10
#LANDSCAPE_APPS_DOMAIN: cfapps.us10.hana.ondemand.com
LANDSCAPE_APPS_DOMAIN: cfapps.eu10.hana.ondemand.com

# the tdash RESTful web service and GitHub interface
MEMORY_TDASH_SERVICE_APP: 1024M
BUILDPACK_TDASH_SERVICE_APP: sap_java_buildpack

SAPUI5 -- Defining Inactivity


There's a subtlety worth going over: How should we define inactivity?  There's a number of browser events that we might connect to in our application to sense when the user is active: web calls, or even mouse movement events are candidates.

Since SAPUI5 is a jQuery/AJAX web application, any user activity will generate jQuery web service calls.  Here I choose to hook into jQuery calls as the way to track user activity.  Let's do just that.

It's straightforward to set this up in a conventionally organized SAPUI5 application -- you will find this structure in most SAP samples and also in Web IDE application templates.  Each application structured in this way will have a top-level Component.js class.  This class is global to the application. Much of the application's OData model is initialized here, so this is a good place to connect our activity tracking.

We'll start by adding these methods and data values at the bottom of the existing class definition:
            /**
* Set to correspond to something less than the SESSION_TIMEOUT value that you set for your approuter
* @see https://help.sap.com/viewer/4505d0bdaf4948449b7f7379d24d0f0d/2.0.04/en-US/5f77e58ec01b46f6b64ee1e2af...
*/
countdown: 840000, /* 14 minutes; SESSION_TIMEOUT defaults to 15 minutes */

resetCountdown: 840000,

/**
* Return number of milliseconds left till automatic logout
*/
getInactivityTimeout: function() {
return this.countdown;
},

/**
* Set number of minutes left till automatic logout
*/
setInactivityTimeout: function(timeout_millisec) {
this.countdown = timeout_millisec;
this.resetCountdown = this.countdown;
},

/**
* Set number of minutes left till automatic logout
*/
resetInactivityTimeout: function() {
this.countdown = this.resetCountdown;
},

/**
* Begin counting tracking inactivity
*/
startInactivityTimer: function() {
var self = this;
this.intervalHandle = setInterval(function() {
self._inactivityCountdown();
}, 10000);
},

stopInactivityTimer: function() {
if (this.intervalHandle != null) {
clearInterval(this.intervalHandle);
this.intervalHandle = null;
}
},

_inactivityCountdown: function() {
this.countdown -= 10000;
if (this.countdown <= 0) {
this.stopInactivityTimer();
this.resetInactivityTimeout();
window.location.href = '/logout';
}
}

Then, somewhere in the class's init() method, we add this passage of code to connect to all OData jQuery calls.

Any time a call is made, we'll reinitialize the countdown timer value -- as long as the user is active, the inactivity counter will never reach zero.
                this.setInactivityTimeout(118 * 60 * 1000);
this.startInactivityTimer();

var self = this;

/**
* Each time a request is issued, reset the inactivity countdown
*/
this.getModel().attachRequestCompleted(function (oEvent) {
self.resetInactivityTimeout();
}, this);

Notice that the inactivity timer is configured to count down to zero at 118 minutes -- two minutes before the session expires at the 120 minute mark. That leaves a pretty comfortable margin for error.  If you alter these, the two values must be properly coordinated for this scheme to work.

The final piece is to add that logged-out.html page.
<!DOCTYPE html>
<html>

<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Logged out</title>
<style>
p.normal {
font-family: "Arial", sans-serif;
text-indent: 150px;
}
a {
font-family: "Arial", sans-serif;
text-indent: 150px;
}
button {
font-family: "Arial", sans-serif;
font-size: 13pt;
padding: 15px 32px;
}
h1 {
font-family: "Arial", sans-serif;
text-indent: 150px;
}
h2 {
font-family: "Arial", sans-serif;
}
h3 {
font-family: "Arial", sans-serif;
}
.login {
text-indent: 150px;
}
.header {
display: block;
padding: 1em 0 .95em;
font-family: "Arial", sans-serif;
font-size: 22px;
background: #007c99;
color: #fff;
-webkit-font-smoothing: antialiased;
}
</style>
<script>
// This function forces a reload of the destination page from the server. It does this by
// supplying a unique argument value with each new invocation.
function refreshMain() {
var unique = new Date().getTime().toString(16);
window.location.href = "/?force="+unique;
}
</script>
</head>

<body>
<div class="header">
<h1>TDash</h1>
</div>

<div class="login">
<h2>You are now logged out</h2>
<p><button onclick="refreshMain()">Return to the application</button></p>
</div>
</body>

</html>

The Heart of the Matter: Approuter Manages Authentication


You will notice this page isn't purely static HTML.

The reason for that is basic to this exercise: in order for Approuter to properly initialize a SAPUI5 application session and OAuth2 tokens, your browser must first issue a plain-old HTML page request to the Approuter.

This is straightforward when your application is first started.  In that case, the very first page visited is index.html -- when the user's browser requests that page and does not present a valid session cookie, Approuter immediately redirects the user to the xsuaa login page.

In this case, however, invoking the /logout URL will tear down and remove the tokens and the session cookie.  That's actually what we want when logging out -- but with browser caching being what it is, it turns out to be a bit tricky to generate a real request from the browser back to the Approuter when we are ready to log in again -- and we must have happen that to kick things off again.

The refreshMain() function does that for us.  Adding the force parameter to the URL serves no other function here than to trick the browser into always actually firing off an HTML request to Approuter.

A Testing Shortcut


The values used in this sample code would be proper for a session that lasts two hours.  That's a long time to wait if you wish to see this code in action.  For testing, I'll often set SESSION_TIMEOUT to 300 (five minutes) and then change the setInactivityTimeout call in Component.js for a two-minute logout:
this.setInactivityTimeout(2 * 60 * 1000);

What's Next?


We have explored how to set up a reliable inactivity logout function when using SAPUI5 with Approuter.  It works well, but this technique could be improved further.  We might, for example, start with a popup warning the user they will be logged out if they don't take action. We could also choose to respond to an impending session expiration with something other than a logout.  Maybe we'll look at such cases in a future article if there's interest in any of those.

In the meantime, if you are looking for deeper information on exactly how to use Approuter and XSA security with SAP Cloud Platform Cloud Foundry, you can check out these useful code samples and tutorials:
26 Comments