Technical Articles
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:
- Ensure the user’s session never expires
- 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)
- 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/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);
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:
- Tutorial: Adding UAA to your Cloud Application Programming Model application
- … which is from the more comprehensive Tutorial Group: Get Started with SAP HANA XS Advanced Native Development
- Code Samples – Approuter/UAA Integration in Java/Spring applications https://github.com/SAP/cloud-security-xsuaa-integration
Hi Riley,
thank you for this great walkthrough. I've just recently had the same browser caching issue which caused issues to do a correct login after being logged out. My solution was to set the http header to no-cache for the landing page. Isn't there such an option in the approuter?
Best regards
Gregor
Hi Gregor,
While I was waiting to hear from the Approuter team I reviewed the current documentation -- I noticed that Approuter actually does appear to support setting the Cache-Control header. I have no idea how recent this addition is, but I'll try it out when I can:
https://help.sap.com/viewer/4505d0bdaf4948449b7f7379d24d0f0d/2.0.03/en-US/5f77e58ec01b46f6b64ee1e2afe3ead7.html
Hi Gregor,
That's a great point. I'm not aware of it being a capability today. I agree that having such HTTP header control would be a great enhancement, I'm asking the Approuter team about that now.
Riley
Hi Gregor,
there is an option inside xs-app.json that i used in our shop solution
regards Holger
Hi Holger,
Exactly. In a follow-up blog post, I mentioned a similar approach.
https://blogs.sap.com/2019/08/23/sapui5-sessions-and-logout-fine-grained-control-of-approuter-caching/
Riley
Hi Riley,
Many thanks for the contribution, the topic is explained quite well and structurally. What I wanted to know is how does the response from server look like after the session is gone. Does it contain defined http return code or some specialized header fields?
We also have struggled with such a problem and our solution on the client side is to watch carefully each response from the SCP and in the case that session is invalid we display a popup about the session should be restored. In case session is invalid the SCP appends response header field "com.sap.cloud.security.login".
Best regards,
Maciej
Hi Maciej,
I can't say that I did much monitoring of the headers, so I don't think I can answer that here. I'll do some research.
The approach I took here might have been swayed by another issue, though: SAPUI5 dynamically loads framework modules. On the way to this solution, I did develop a different scheme that would look at the response code to deal with a failing jQuery call. However, SAPUI5's dynamic module loading was giving me fits: if a user's session expired and they then happened to click on a UI feature requiring a previously unreferenced module, a completely different error handler inside the UI5 framework was invoked. Because of that, I came to the conclusion that, short of completely replacing approuter with my own design, I would simply use a scheme that prevents sessions from timing out in the middle of the application via automatic logout.
Riley
Great blog post that helped us to implement the mandatory logout functionality that has to be present in our apps (security requirement). Thanks Riley !
My pleasure, Radu!
Hi Riley Rainey ,
Thanks for the great blog.
Is there any way to forward the cookies (JSESSIONID and __VCAP_ID_) to the backend from approuter ?
BR,
Satvindar
I expect so. I'll check. Wondering: what would you do with those, though?
Hi ,
The issue is Gorouter is generating a session cookie for 2 app instances but the same cookie is not getting forwarded to Gorouter from the app router which is resulting in a 403 forbidden error for all update/create calls.
Is there any way we can handle such a situation from the app router as the backend is handling the CSRF protection?
Thanks and regards,
Satvindar
Hi,
I am facing the same issue - 403 Forbidden for my POST calls.
Were you able to get the cookies JSESSIONID and __VCAP_ID_ ?
Thanks,
Shruthi
Hi,
POST calls for a single app instance were always working. For multiple instances, we modified our backend implementation. Not sure about your scenario, if you can share more details then I can help you.
Regards,
Satvindar
Hi Riley,
Thank you so much for the detailed blog.
I was struggling with the session time out especially with the caching.
This blog and the follow-up blog post helped me a lot to achieve a perfect session time out and also Client-initiated central logoff functionality.
Regards,
Ravindra
Hi Ravindra,
I’m glad these articles helped you. It's always great to hear from someone who has used it.
Cheers!
Hi Riley,
How to handle session refresh for a timed out session if the session of UI5 application is opened in multiple tabs of browser? After re-validating the expired session in one tab the other tab(s) should be refreshed and return to the app's page instead of calling the logged-out.html page.
Appreciate your help.
Thanks & Regards,
Ravindra
Hi Ravindra,
That's an interesting problem perhaps without a quick solution.
Our experience has been that this isn't an issue, but that not to suggest your application might be designed in a different very valid way that would lead to problems.
One solution might be to avoid logging out and, instead, force a session refresh instead. That could be accomplished by making an Ajax-style call in the background to almost any application endpoint.
A more elegant solution might be to use the browser's Local Storage or Broadcast Channel APIs to coordinate tracking of session expiration between tabs. I might look into that.
Riley
Thanks Riley. That really makes sense.
BR,
Ravindra
Hi Riley,
Everything was working as expected but suddenly the logout stopped working and from the logout page(on click of Return to application button) it again navigates back to application index.html page without going for re-login(XSUAA login page).
Seems the issue started when we switched to OIDC(OpenID Connect) from SAML2.0 in trust configuration with SAP IAS Tenant.
If i switch back to SAML2.0 it works as expected.
Do you have any clue around this issue with OIDC?
Regards,
Ravindra
Great Post!
v4.ODataModel however doesn't support the attachRequestCompleted event.
Any suggestion how to get it working using v4.ODataModel?
Regards,
Tom
Hi Tom,
I'm doing some internal research. Stand by, please.
It's a bit of a hack but you could add a call resetInactivityTimeout() in each place where you receive a data response. That might require surfacing the parent class globally - that's a really off-the-cuff idea, I am unsure how hard (or ugly) the result might be.
Riley
Also wondering if it might be possible to hook into completion events at the Ajax level.
So far, I have only been able to confirm that the UI5 OData V4 class did, in fact, drop support for requestCompleted events back at 1.37.0.
Thanks for getting back to me. Hooking into Ajax completion events is possible, however a bit of hassle if you have many.
But I from what I know so far I don't see an alternative solution. Strange as SAP is pushing to move to V4 (e.g. Cloud Foundry) but on the other hand is removing functionality that would make life a lot easier.