In the past, I've dealt with creating dynamic session timeouts in my ColdFusion applications. I've typically done this to minimize the memory footprint created by site traffic spikes caused by bots (such as the GoogleBot) that spider the web site. Sometimes, however, I see that people want to delay session persistence in their ColdFusion applications for standard users as well as bots - the caveat being that the user should have full session functionality after they've logged into the given web site. Since I've never used this specific approach before, I figured I would give it a shot.
A user's session duration is determined by the SessionTimeout property established in the Application.cfc ColdFusion framework component. This timeout is defined on a per-page-request basis. This means that a user can have one session timeout on one page request and then a completely different session timeout on a subsequent page request. This flexibility is part of what makes the ColdFusion framework so incredibly powerful and happens to be the feature we'll leverage to create delayed persistence sessions.
At the time the session timeout is defined - in the pseudo construct of the Application.cfc component - we don't have access to the user's session. This is because the current page request is not yet associated with ColdFusion's application memory space. As such, we cannot rely on session-based information. What we can rely on, however, is cookie information. At the time the Application.cfc pseudo constructor runs, ColdFusion has already parsed the HTTP request and initialized the cookie scope. As such, we will use a cookie-based flag to determine the timeout of the user's session based on the current page request.
Typically, when dealing with dynamic session timeouts, I like to give every user a session, even if it's one that times out quickly. This allows the rest of the page request to operate under the assumption that a session exists. While this is not necessary, I have found that it makes site-wide logic easier to program. That's why, in the following Application.cfc, you'll see that I'm not turning on and off session management, but rather adjusting session timeouts.
<cfcomponent output="false" hint="I define application settings and event handlers."> <!--- Define the application settings. ---> <cfset this.name = hash( getCurrentTemplatePath() ) /> <cfset this.applicationTimeout = createTimeSpan( 0, 0, 10, 0 ) /> <cfset this.sessionManagement = true /> <!--- By default, all of our new sessions will be given a very short timeout. This will be true for all users, spiders, and bots. We want session to always be enabled since our page request might require it. ---> <cfset this.sessionTimeout = createTimeSpan( 0, 0, 0, 2 ) /> <!--- Session timeout is going to be dictated by a cookie that we set on the user. If the cookie key exists, then this user is not using "persisted" sessions. ---> <cfif structKeyExists( cookie, "persistSession" )> <!--- This user is now using fully-enabled sessions. Let's change the session timeout at this point to be more agreeable with standard usage. To do this, all we have to do is override the existing sessionTimeout. NOTE: We are going from 2 seconds to 5 minutes. ---> <cfset this.sessionTimeout = createTimeSpan( 0, 0, 5, 0 ) /> </cfif> <!--- Define page request settings. ---> <cfsetting requesttimeout="10" showdebugoutput="false" /> <cffunction name="onSessionStart" access="public" returntype="void" output="false" hint="I initialize the session."> <!--- Set up a hit count variable so that we can see how many page requests are recorded in this user's session. ---> <cfset session.hitCount = 0 /> <!--- If the user already has full session persistence, call the delayed session initialization. This would happen if a user with an expired session returned to the site (with a non-expired persistSession cooke). ---> <cfif structKeyExists( cookie, "persistSession" )> <!--- Fully initialize user's session. ---> <cfset this.onSessionStartDelayed() /> </cfif> <!--- Return out. ---> <cfreturn /> </cffunction> <cffunction name="onSessionStartDelayed" access="public" returntype="void" output="false" hint="I intialize the delayed session (once the user has logged-in as is using full session capabilities)."> <!--- This is just an idea - you might want to put further session initialization information here so that it's in a centralized place. ---> <!--- Return out. ---> <cfreturn /> </cffunction> <cffunction name="onRequestStart" access="public" returntype="boolean" output="false" hint="I initialize the page request."> <!--- Increment the session hit count. Notice that we can act here regardless of the current session timeout because every page request is guaranteed to have a session (even if it only lasts 2 seconds). ---> <cfset session.hitCount++ /> <!--- Return true so the page can process. ---> <cfreturn true /> </cffunction> </cfcomponent>
As you can see, all users start off with a session timeout of 2 seconds. Then, based on the existence of the cookie - persistSession - the session is alternately set to 5 minutes (the full length of a standard user's session timeout). This ColdFusion framework component has the standard onSessionStart() event handler that will be called every time a session starts. I have also added an additional event handler called onSessionStartDelayed(). This is where you can put any additional initialization logic that will be needed when switching the user over to full session functionality.
You'll notice that onSessionStart() event handler checks to see if the persistSession cookie exists. If it does, it turns around and explicitly executes the onSessionStartDelayed() event handler. This logic is put in place in case where a user returns to the site with an expired ColdFusion session and a non-expired persistSession cookie. In this way, we won't be depending on the "login" work flow to give user's appropriate session functionality.
To test the session persistence behavior, I've defined a basic hitCount variable that will be incremented with each page request. I then output this value in the demo index.cfm page:
<cfoutput> <h1> Delaying ColdFusion Session Persistence </h1> <p> Hit Count: #session.hitCount# ( <a href="index.cfm">refresh</a> ) </p> <p> Persist Session: #structKeyExists( cookie, "persistSession" )# </p> <p> <a href="login.cfm">Login</a> | <a href="logout.cfm">Logout</a> </p> </cfoutput>
Here, I am outputting the hitCount as well as the existence of the persistSession cookie. As I refresh this page quickly, the hitCount goes up. But, if I delay too long (beyond the ~2 second session timeout), my hitCount is reset. This will be the standard behavior of the non-logged-in user.
Now, let's take a look at the login page - login.cfm. This is where will we be logging the user into the system and altering their session functionality.
<!--- When we log the user in, the first thing we want to do is set the session persistence flag. This will convert the user's session from a short-term to a longer-term engagement. NOTE: We do NOT want this cookie to expire before the session times out. This would cause very odd behavior if the user were to come back to the site within the session timeout limit (after this cookie expired). ---> <cfcookie name="persistSession" value="true" expires="never" /> <!--- Now that we are using a full session, perform any further initialization that might be required on this session object. To do this, we will invoke the onSessionStartDelayed() method on the ColdFusion framework component, Application.cfc. NOTE: We are leveraging the Application.cfc component to centralize the session initializtion logic (in case this needs to be altered or re-distributed later on). ---> <cfset createObject( "component", "Application" ) .onSessionStartDelayed() /> <!--- Redirect back to index page. ---> <cflocation url="index.cfm" addtoken="false" />
The logic on this page is very minimal so as to focus on the task at hand. When logging the user into the system, the first thing we do is set the cookie, persistSession. If you remember from earlier, it's this cookie that determines the user's session timeout in the Application.cfc pseudo constructor. For this demo, I've set the cookie to never expire. This is not really what we want to do, but for our purposes, it was the best option. We could have created a session cookie which expires when the user closes the browser; or, we could have created a cookie with a more explicit expiration date. Since cookies and sessions are managed differently by the ColdFusion framework, however, neither of these later two approaches really fits the bill. Ideally, both our session IDs and this cookie would be created as session cookies that would simultaneously expire when the user closed their browser.
Cookie expiration details aside, once the cookie is set, we then create an instance of the Application.cfc ColdFusion framework component and invoke the onSessionStartDelayed() event handler. This will execute any additional session initialization logic that needs to be applied. The reason I perform additional initialization in this manner, rather than directly in the current page, is because I like to keep all the framework-based initialization code inside the ColdFusion framework. Plus, if this code needed to be executed from several different places, this centralization prevents duplication of business logic.
Once the cookie is set and the session fully initialized, I then forward the user back to the index.cfm page. While this was not true in older versions of ColdFusion, CFLocation calls successfully pass all of our cookie information back to the client. As such, the persistSession cookie is persisted to the user's browser and is subsequently passed back to the ColdFusion application on the next page request (where the session timeout is dynamically defined).
To log the user out of the application, the logout.cfm page simply expires all of the cookies that have been set. This will include our persistSession cookie as well as our ColdFusion session tokens.
<!--- To clear the session, all we really need to do is clear all the cookie values (since in this case, CFID and CFTOKEN are being stored as cookies). NOTE: This will also expire our "persistSession" cookie flag that is used to determine session timeout. ---> <cfloop item="name" collection="#cookie#"> <!--- Expire this cookie. ---> <cfcookie name="#name#" value="" expires="now" /> </cfloop> <!--- Redirect back to index page. ---> <cflocation url="index.cfm" addtoken="false" />
Once the persistSession cookie is expired, subsequent page requests to the ColdFusion application will lead to the original, short session timeout of 2 seconds.
I've never used this approach personally, so I am sure there are some points that I'm missing. The one area that I know I am not 100% happy with is the way in which the persistSession cookie is set; using a "never" expiration approach means that a return user with an expired session will immediately be given full session functionality. Of course, if a user has previously presented with a desire to login, this might not be such a bad thing. At the very least, I hope this leads to some good conversation.
Brilliant. Absolutely brilliant.
Mostly, I've ignored this issue in the past, and have only thought about handling it with other (more complex) methods.
Your elegant solution that should be standard procedure on every application. Great work!
Wow - thanks :) I really appreciate that kind of feedback!
This approach is a little problematic for a site that has many apps using this methodology, purely due to the potential browser cookie limits, but even then, not impossible. I could see having one shared cookie between all apps that just add their name to the shared cookie as a delimited list, then just change the logic of your if statement to check for the existence of the cookie AND a ListFind() to check for the value of the particular app's name or ID in the cookie's value.
Since the name of the Application (this.name) is known at every crucial point, I suppose you could just use the application name to create the cookie flag. That way, each app would have it's own flag.
I like the approach, but you need to be conscious of the security ramifications. As it stands, someone could easily just add a persistSession cookie to their HTTP request and force the onSessionStartDelayed to execute.
In order to secure it, you should have some sort of validation routine. Unfortunately, that pretty much requires that you create a unique value (like a UUID) for the cookie and store that value in the application scope or in the database in order to be able to validate it. I don't like that approach though, because it mixes session and application persistence.
Jason Dean has a great ongoing discussion on this very topic at http://www.12robots.com/index.cfm/2009/1/27/New-Session-on-Login--Security-Series-1231-and-641 .
I am not sure that the "persistSession" really creates a security concern. To me, the whole idea of delaying a full-session-boot is only about performance end memory usage. I didn't imaging anything "special" actually taking place in the onSessionStartDelayed(). After all, the user would still need to log into the system to get any logged-in benefits.
Thanks for Jason's link - I'll definitely be checking it out. But, I hear what you're saying about the UUID storage. I suppose you could base it on the IP address encryption or something - that way you wouldn't have to store something... I think.
But, like I said on the post, I have never actually done this in real life - this was just a thought experiment. I'll do some more thinking on the security aspects of this approach.
I guess I mention it becuase of the way that I personally use onSessionStart. In my world, onSessionStart is where I do all of the session setup, but only after a user has been authenticated. If the user isn't authenticated, onSessionStart just gets "skipped" effectively (and we set the session timeout to 1 second so it doesn't stay around, just like you do). Everything in our application requires an authenicated logon, so with no authentication, you don't even get a session.
So you're correct - the sensitivity of onSessionStartDelayed depends entirely on your session management paradigm.
May I ask how you skip the session initialization when the user first gets to the site? Do you start off with session management turned off to being with?
If this is proprietary info, that's totally understandable.
Could the "path" part of a cookie solve this problem? If we're talking about storing some token in the cookie that limits its use to a particular app, that seems like what the path was created for.
That sounds like it could be spot on. I've never actually limited a cookie to a domain or path before, so I can't say from personal experience. But, that certainly sounds right.
Great solution Ben! We take a different approach:
When a user makes a request we use an algorithm identifies it as a bot by analyzing user provided information (cookies, user agent, CGI, IP, reverse name lookup, etc..) and browsing habits (how fast are requests coming, how many requests made, headers provided, etc..).
If it is detected as a bot, the session scope is cleared at the end of the request.
We catch most bots in 2 requests, though the more tricky scrappers take a few more.
Here is a code snippet to get rid of the user's session that we use:
<cflock type="exclusive" scope="session" timeout="5">
<cfset structClear(SESSION) />
<cfset getPageContext().getSession().invalidate() />
It sounds like you have a really sophisticated system over there - really thorough on all the checks. That session invalidate thing looks interesting; does that actually get ColdFusion to end the current session?
Thanks Ben, this is *exactly* what I was looking for and it answered all my questions :) I customized it so that obvious bots get 2 seconds, anonymous people 20 minutes and logged in people 40 minutes. I need to allow anonymous users to shop around, put items in their session based cart and *then* register/login. I am planning to use session only cookies because with my traffic I'm not too concerned that logged off/session-timed-out users keep roaming the site and consume a bit more memory.
Fantastic! Sounds like a solid approach. I hope this works out for you.
I've tried playing around with the getSession() method and it never seems to return anything for me - I keep getting a NULL return. Are you setting something else to enable this method?
I see - it looks like you have to use J2EE session management in order for that to work. From the livedocs:
If you use J2EE session management, you can invalidate the session, as follows:
We do use J2EE sessions, as we do share some sessions with java applications. If you don't use J2EE sessions you can try some undocumented features in CF to end a session (try setMaxInactiveInterval()). However, we had some issues with this not working on CF9, perhaps it was because we were using J2EE sessions (didn't think about that).
FYI, we went with the approach of ending sessions when invalid rather than the other way around because we track all user requests in a history collection object that recommends relevant/similar pages based on history (though it keeps a "rolling" history to prevent buffer overflow attacks).
Going the route of a cookie based approach will end up with the first request always being "missed" since the cookie variable cannot be read until the second request.
The SESSION.setMaxInactiveInterval() is an interesting approach; I recently explored it among a few different ways to try to kill session.
I am not sure what you mean about the cookie approach and the first page? You should be able to set cookies and read them in the same request?
I've been getting errors from the getPageContext().getSession().invalidate() stuff - despite having J2EE sessions enabled - but apparently due to the getSession() call returning null.
I think I've solved it with the following:
<cfset jSession = getPageContext().getSession() />
<cfset jSession.invalidate() />
But not really sure - there's plenty of places recommending the invalidate call but not seen any of them mentioning the possibility of a null result.
Anyone experienced this before, and know if the above code will solve the problem?
Because I haven't been using J2EE sessions, I was never able to get the invalidate() method to work.
Hmmm, I didn't get any email notification here - I definitely selected all the checkboxes (they're still ticked).
Sorry about that; sometimes my mail gets blocked by the other servers. Something about not having the right HELO handshake in my email headers or something. I need to figure it out, but every time I try to, it just seems very complicated.
Well here is my 2c worth. This all looks way too technical for a reverse cell phone number lookup post.