SAPUI5, SAP ERP

SAPUI5 Applications with Approuter: Sessions and Automatic Logout

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”: “usuaa” 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/5f77e58ec01b46f6b64ee1e2afe3ead7.html
             */
            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);

Leave a Reply

Your email address will not be published. Required fields are marked *