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.

6 comments:

Kyle Benson said...

Thanks for sharing, Jim! This is great and I feel it would be a must when implementing Desktop SSO.

Jim Marion said...

@Kyle, exactly. I forgot to mention that. With SSO, especially the Kerberos SDK, signout doesn't make sense.

Stephen Phillips said...

For what it's worth, the PSChrome extension has something like this baked in. I think they call the feature timeout evasion, but I assume it is the same kind of code. I have noticed it still timing out every once in a while, but I assume there is some bug with attaching to the page at times.

Anyway, a Chrome extension is a way of doing it without making the change system wide.

Jim Marion said...

@Stephen, can you open your JavaScript console and record what happens? I would like to know if you see an error when the timeout happens. Perhaps there was a JavaScript error that prevented the code from registering or there was a server error? The JavaScript resets the original timeout if there is an Ajax error and it is not able to reset the timeout.

Stephen Phillips said...

@Jim,

Sorry it took so long to respond. I was out on vacation and didn't have time to experiment and respond.

I opened up a tab to a run control and left it while I went to dinner. I checked the setting on the console to preserve the log across page loads. When I came back, I had this error message:

Uncaught TypeError: undefined is not a function PT_NAV2_JS_1.js:776
Error in event handler for (unknown): Cannot read property 'split' of null
Stack trace: TypeError: Cannot read property 'split' of null
at eval (eval at (chrome-extension://cpgoncheakfjhldfbebekijoeaabnfeb/includes/content.js:1:169), :3344:34)
at Array.callbackFunction (eval at (chrome-extension://cpgoncheakfjhldfbebekijoeaabnfeb/includes/content.js:1:169), :3548:9)
at Array. (chrome-extension://cpgoncheakfjhldfbebekijoeaabnfeb/includes/content.js:23:124)
at chrome-extension://cpgoncheakfjhldfbebekijoeaabnfeb/includes/content.js:13:292
at Function.target.(anonymous function) (extensions::SafeBuiltins:19:14)
at EventImpl.dispatchToListener (extensions::event_bindings:397:22)
at Function.target.(anonymous function) (extensions::SafeBuiltins:19:14)
at Event.publicClass.(anonymous function) [as dispatchToListener] (extensions::utils:93:26)
at EventImpl.dispatch_ (extensions::event_bindings:379:35)
at EventImpl.dispatch (extensions::event_bindings:403:17) extensions::uncaught_exception_handler:9
Uncaught TypeError: undefined is not a function


It actually showed up in the console twice.

The instance that I am currently working in is Tools version 8.52.17. It shouldn't matter but it is running on a SQL Server database. I assume the web server and app server are running on Windows.

I hope that is helpful. I don't know about fixing the problem myself. I haven't found the source code published anywhere. I did find Shelby Melban's site, and he has a blog. Maybe if I find the error in the javascript, I can comment somewhere on his blog.

Jim Marion said...

@Stephen, that error appears related to the hover/bread crumb menu. I suggest you add some console.log statements to your JavaScript to show that the timeout was configured, etc, so you can confirm that all of the custom code is firing as expected. You also might want to print something on the Ajax response to make sure it is successful.