Thursday, July 24, 2014

Unlimited Session Timeout

There are a lot of security admins out there that are going to hate me for this post. There are a lot of system administrators, developers, and users, however, that will LOVE me for this post. The code I'm about to share with you will keep the logged in PeopleSoft user's session active as long as the user has a browser window open that points to a PeopleSoft instance. Why would you do this? I can think of two reasons:

  • Your users have several PeopleSoft browser windows open. If one of them times out because of inactivity at the browser window level, then it will kill the session for ALL open windows. That just seems wrong.
  • Your users have long running tasks, such as completing performance reviews, that may require more time to complete than is available at a single sitting. For example, imagine you are preparing a performance review and you have to leave for a meeting. You don't have enough information in the transaction to save, but you can't be late for the meeting either. You know if you leave, your session will time out while you are gone and you will lose your work. This also seems wrong.

Before I show you how to keep the logged in user's session active, let's talk about security... Session timeouts exist for two reasons (at least two):

  • Security: no one is home, so lock the door
  • Server side resource cleanup: PeopleSoft components require web server state. Each logged in user session (and browser window) consumes resources on the web server. If the user is dormant for a specific period of time, reclaim those resources by killing the user's session.

We can "lock the door" without timing out the server side session with strong policies on the workstation: password protected screen savers, etc.

So here is how it works. Add the following JavaScript to the end of the HTML definition PT_COMMON (or PT_COPYURL if using an older version of PeopleTools) (or even better, if you are on PeopleTools 8.54+, use component and/or role based branding to activate this script). Next, turn down your web profile's timeout warning and timeout to something like 3 and 5 minutes or 5 and 10 minutes. On the timeout warning interval, the user's browser will place an Ajax request to keep the session active. When the user closes all browser windows, the reset won't happen so the user's server side session state will terminate.

What values should you use for the warning and timeout? As low as possible, but not so low you create too much network chatter. If the browser makes an ajax request on the warning interval and a user has 10 windows open, then that means the user will trigger up to 10 Ajax requests within the warning interval window. Now multiply that by the number of logged in users at any given moment. See how this could add up?

Here is the JavaScript:

(function (root) {
    // xhr adapted from http://toddmotto.com/writing-a-standalone-ajax-xhr-javascript-micro-library/
    var xhr = function (type, url, data) {
        var methods = {
            success: function () {
            },
            error: function () {
            }
        };

        var parse = function (req) {
            var result;
            try {
                result = JSON.parse(req.responseText);
            } catch (e) {
                result = req.responseText;
            }
            return [result, req];
        };

        var XHR = root.XMLHttpRequest || ActiveXObject;
        var request = new XHR('MSXML2.XMLHTTP.3.0');
        request.open(type, url, true);
        request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
        request.onreadystatechange = function () {
            if (request.readyState === 4) {
                if (request.status === 200) {
                    methods.success.apply(methods, parse(request));
                } else {
                    methods.error.apply(methods, parse(request));
                }
            }
        };
        
        request.send(data);
        return {
            success: function (callback) {
                methods.success = callback;
                return methods;
            },
            error: function (callback) {
                methods.error = callback;
                return methods;
            }
        };
    }; // END xhr


    var timeoutIntervalId;
    var resetUrl;

    /* replace warning message timeout with Ajax call
     * 
     * clear old timeout after 30 seconds
     * macs don't set timeout until 1000 ms
     */
    root.setTimeout(function () {
        /* some pages don't have timeouts defined */
        if (typeof (timeOutURL) !== "undefined") {
            if (timeOutURL.length > 0) {
                resetUrl = timeOutURL.replace(/expire$/, "resettimeout");
                if (totalTimeoutMilliseconds !== null) {
                    root.clearTimeout(timeoutWarningID);
                    root.clearTimeout(timeoutID);
                    
                    timeoutIntervalId =
                            root.setInterval(resetTimeout /* defined below */,
                                    root.warningTimeoutMilliseconds);
                }
            }
        }
    }, 30000);

    var resetTimeout = function () {
        xhr("GET", resetUrl)
                .success(function (msg) {
                    /* do nothing */
                })
                .error(function (xhr, errMsg, exception) {
                    alert("failed to reset timeout");
                    /* error; fallback to delivered method */
                    (root.setupTimeout || root.setTimeout2)();
                });
    };
}(window));

A special "shout out" to Todd Motto for his Standalone Ajax/XHR JavaScript micro-library which is embedded (albeit modified) in the JavaScript above.