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.

24 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.

DigitalEagle (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.

DigitalEagle (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.

Tom Mannanchery said...

Hi Jim,

Does the presence of a pagelet with auto refresh enabled on a homepage impact expiration? I have a pagelet which has an auto refresh of 15 seconds (configured as described here: http://docs.oracle.com/cd/E41633_01/pt853pbh1/eng/pt/tprt/task_UsingAttributestoEnhancePagelets-717aa0.html#ConfiguringAutomaticPageletRefresh-717a97). I have timeout/expiration on the web profile set to 2 minutes. I put the pagelet on a homepage and see leave the homepage inactive for 2 minutes, but my session doesn't time out.Is this expected behavior?

Thanks,
Tom

Jim Marion said...

@Tom, I don't know if that is expected behavior or not. It makes sense though. Session timeout is based on the server's session state. If the browser doesn't contact the server within the timeout, then the server session will expire. It makes sense that a pagelet constant refresh would keep it active. Whether that is intentional from PeopleTools, I don't know. Sounds like something that should be in the documentation, so it would be helpful if you created a support case for this.

Tom Mannanchery said...

Hi Jim,

I have created the SR 3-11218357701 for this and the Support Engineer confirmed your theory. Thanks a bunch for helping with this.

Tom

Unknown said...

I have a requirement to update a table when the user is getting timedout. Normally the PSACCESSLOG table is updated on the timeout. Is there a way to trap an Event which is doing this also use this place to do my update on the custom table?

Chase London said...

Hi Jim,

I recognize this one is a bit older, but I hope you are still looking at it. I've been experimenting with this on a portal environment. The portal session seems to stay active, but the non-portal connects seem to go dead. Does that make any sense? I'm guessing the peoplesoft session cookies are expiring on the back end, and that they are not renewed by the AJAX script.

Thoughts?
Thanks!
Chase

Jim Marion said...

@Chase, hope all is well. Glad to hear from you. What you are saying makes sense, I can see that. You may have to hit all of the content providers on interval. Considering that a session created in Interaction Hub may not necessarily create a session in each content provider, this may create more sessions than necessary on content providers you will never visit. For example, if you log in to work HCM transactions, but don't plan on touching FSCM, then having the reset code also ping FSCM may create a session on FSCM. I'm not sure.

Michael Todd said...

Jim,

We have PeopleSoft ELM. When Learners launch training, it may be open for hours and ELM often times out behind the training. So this would be perfect for our use.

You mentioned that, using 8.54 or above, you could use Branding to apply this javascript. I have put some javascript into headers.

How would you go about adding it to PT_COMMON using branding?

Thank you,

Michael

Jim Marion said...

@Michael, I would use the branding global system options to inject into Post 8.54 instances. PT_COMMON is for pre-8.54. From the branding system options, you can specify JavaScript and CSS directly and that gets injected into every page.

lifeandshit said...

Dear Jim,
I know this is completely off topic. But has any university implemented RFID with peoplesoft?

John B said...
This comment has been removed by the author.
John B said...

Hi Jim,

I had done something very similar at a client 10 years ago in tools 8.49, but they lost the code over the years. I am reimplementing this logic and found your example while looking for some sample code.

My solution seems simpler--maybe too simple--so here is what I did.

In PeopleTools->Portal->Branding->Define Headers and Footers in our custom Header, I added a basic HTML container at the bottom. Here is the code:

OPENBRACKET script type="text/javascript" language='JavaScript' CLOSEBRACKET
function keep_alive() {
http_request = new XMLHttpRequest();
http_request.open('GET', timeOutURL.replace("expire","resettimeout"));
http_request.send(null);
};

setInterval(keep_alive,300000); //Ping every minute
OPENBRACKET /script CLOSEBRACKET

Every 5 minutes, this will just run the resettimeout command.

Do you see any problems with this method?

Thanks

Jim Marion said...

@John, I don't see you resetting the session timeout interval. Your Ajax call will still keep the server session active, but what are you doing to ensure the PeopleSoft JavaScript doesn't log you out? An issue I'm wondering about the branding framework is about injection location. Doesn't that only inject into the header? I thought the reset code existed on the content page as well. I would think that JavaScript would attempt to display the timeout warning and log out there session.

John B said...

You are correct, I'm not actually resetting the session timeout interval. Somehow, the resettimeout command must be handling that. I have added this code to ping every 5 minutes, and I have configured all of our timeouts at 6 minutes, and our pages stay active all night.

You bring up an interesting point about the point of injection. If I change the URL from /psp/ to /psc/, the code is not injected and the page times out after 6 minutes.

I'm thinking this will be a problem for me, because we have work centers that cross environments.

So from what I can tell, my javascript is solid, it resets timers as needed and keeps sessions open. It even keeps state intact on the web server. We are on PT 8.54. Is there somewhere else I can inject this, so it will get into every page? I actually implemented the MonkeyGrease solution 10 years ago, but I'm sure there is something much simpler today.

Jim Marion said...

@John, PT 8.54+ has branding system options that allows you to inject JavaScript into all PeopleSoft pages. I would use that. JavaScript in branding system options executes for both the header and the content, so you will want to make sure you aren't hitting the server twice in 5 minutes by adding logic to your JavaScript.

You would need to do this in all content providers, not just in the one system with the WorkCenter or branding header.

Unknown said...

I was playing around with this js. I created an alternate header, js branding object with the js, and placed that in an alternated theme, and assigned branding by role. The delivered theme is on by default, and a role triggers the alternate header/timeout evasive js. I assigned myself the alternate role, and I’m wondering: how do I know the timeout evasion js is being invoked? That it's working, besides doing nothing and waiting for the session seemingly not timing out? Can I find the js in chrome web console? fiddler traffic?

Jim Marion said...

Yes! You will see it as an XHR request in Chrome's network tab or as a request in Fiddler. And yes, you have to wait for the timeout to see it. For testing purposes, I would set the web profile timeout really low (3 minutes).

kane81 said...

Hi,

Tad off topic, but related.
I want to display a count down timer on the banner to show when the user's session will timeout.

I am using PS_TOKENEXPIRE to mark when the countdown starts/resets which works fine - however when the default warning session timeout popup comes up - a new PS_TOKENEXPIRE is being issued, so my counter thinks it should restart rather than resuming...

Any suggestions on how I can overcome this issue or different ways to implement the countdown timer?

Thank you

Jim Marion said...

One idea is to hook into the PeopleSoft setupTimeout2 JavaScript function by replacing it with my own.