The ColdFusion application lifecycle can be a tricky beast to understand because it hinges more on memory space association and less so on what you might traditionally consider a "running" application. As such, the concept of actively "ending" or "killing" a ColdFusion application or user session is not something that lends itself well to ColdFusion application framework. ColdFusion 9 introduced the ApplicationStop() function to aid in its new Object-Relational Mapping Hibernate integration; but even with this new function, application and session lifecycle still act completely independently.
With all the fuzziness about what it means to be a ColdFusion application, I sometimes see people make the assumption that they can end a user's ColdFusion session simply by clearing the Session scope. Unfortunately, this is not true and will only lead to corrupted sessions. If you've used this approach before and it worked (I know I have), it most likely worked by sheer coincidence; meaning, you probably cleared a business-logic-identifier that signaled to your business logic that the user was no longer "logged in." The user's ColdFusion session, however, carried on quite well regardless.
The problem with clearing the Session scope is that the Session scope is not what relates the user to your ColdFusion application; just the opposite in fact - the Session scope is what is given to the user after that relationship has been established. The entities that relates a user to an active session are the CFID and CFTOKEN values (yes, this is a bit different if you enable J2EE sessions). These values persist as cookies and are passed, by the browser, back to the server with every single page request. It is these cookies that associates a user to a given session, which then in turn, makes available a given Session scope.
To demonstrate this, I have set up a simple ColdFusion application that approaches session destruction in two ways: clearing the Session scope and expiring the cookies. Let's take a quick look at the Application.cfc to see how the application has been defined:
<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, 5, 0 ) /> <cfset this.sessionManagement = true /> <cfset this.sessionTimeout = createTimeSpan( 0, 0, 0, 30 ) /> <!--- 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 /> <!--- Return out. ---> <cfreturn /> </cffunction> <cffunction name="onRequestStart" access="public" returntype="boolean" output="false" hint="I initialize the page request."> <!--- Increment the session hit count (if it exists). ---> <cfif structKeyExists( session, "hitCount" )> <!--- Increment. ---> <cfset session.hitCount++ /> </cfif> <!--- Return true so the page can process. ---> <cfreturn true /> </cffunction> </cfcomponent>
There's nothing special going on here. When the session starts up, I am creating a hitCount variable to record page requests. Then, in the onRequestStart() event handler, I simply increment that hitCount value; of course, since we are experimenting with clearing Session scopes, I am checking to see if that value exists before I increment it.
Now that you see how the application is set up, let's take a look at our main display page:
<cfoutput> <h1> Clearing The ColdFusion Session Scope </h1> <p> Hit Count: <!--- Make sure the hitCount exists - if we cleared the session scope, then this will not be there. ---> <cfif structKeyExists( session, "hitCount" )> #session.hitCount# <cfelse> <em>N/A</em> </cfif> ( <a href="index.cfm">refresh</a> ) </p> <p> <a href="clear.cfm">Clear Session</a> | <a href="rebuild.cfm">Rebuild Session</a> | <a href="expire.cfm">Expire Cookies</a> </p> <!--- Dump out the sesion for debugging. ---> <cfdump var="#session#" label="Current Session" /> <br /> <!--- Dump out the cookie for debugging. ---> <cfdump var="#cookie#" label="Current Cookies" /> </cfoutput>
On this page, not only are we outputting the hitCount value stored in the session, we are also outputting the Session and Cookie scopes to see what value are available. When we hit this page for the first time, we get the following output - take special note of the where the CFID and CFTOKEN values show up:
As you can see, the CFID and CFTOKEN values are available in both the Session and Cookie scopes. Don't let that distract you, though - the session-scoped ones are not all that important. Take note that the current CFID value is 17603.
Ok, now, let's try to clear the Session scope by clicking on the "Clear Session" link. That will bring us to this page:
<!--- Clear the session scope. All this does is clear the contents of the session scope; it has no bearing on the person's session from an interaction standpoint (other than you might start breaking code). ---> <cfset structClear( session ) /> <!--- Redirect back to index page. ---> <cflocation url="index.cfm" addtoken="false" />
As you can see, this page simply clears all variables from the user's associated session object and then relocates them back to the primary display page. Sometimes, the assumption is made that this will "end" a user's session; but, when we get back to the display page, you can see that it does not:
If the use of structClear() actively ended the user's ColdFusion session, then this subsequent request to the application would have initiated a new session and a new call to the onSessionStart() application event handler. As you can see, however, the Session scope has not been repopulated; the user's previous ColdFusion session is still "running", albeit with a severely crippled Session scope.
As I mentioned before, the user's session is still running because the Session scope is a byproduct of the session and not the mechanism behind it. The drivers of the mechanism are the CFID and CFTOKEN cookies which, as you can see above, are still being passed successfully after the Session scope has been cleared. For fun, we can even try to rebuild our previous Session scope based on these cookies. By clicking on the "Rebuild Session" link, we'll be taken to this page:
<!--- Since we completely cleared out our session scope, in order to rebuild it, we pretty much have to go back to the source: our Application.cfc. Let's create an instance of it and use it to re-populate our session with the **programmer** defined session information. ---> <cfset app = createObject( "component", "Application" ) /> <!--- Rebuild the programmatic session. ---> <cfset app.onSessionStart() /> <!--- Using the onSessionStart() event handler will only re-populate the session with programmer-defined information, we have to try to put back in some of the automatic data that the ColdFusion framework would have created during session initialization. ---> <cfset session.cfid = cookie.cfid /> <cfset session.cftoken = cookie.cftoken /> <cfset session.sessionid = "#app.name#_#cookie.cfid#_#cookie.cftoken#" /> <!--- Redirect back to index page. ---> <cflocation url="index.cfm" addtoken="false" />
Once we've cleared the Session scope, we can't get all of those original values back; what we can do, however, is put the Session scope back into a non-corrupted state. To do this, we need to do two things: re-initialize the Session scope and re-populate the CFID and CFTOKEN values. As you can see above, in order to re-initialize the Session scope, I am creating an instance of the Application.cfc ColdFusion component and then manually invoking the onSessionStart() class method. Because this method refers to the Session scope directly, it will work on the user's current Session. To get the CFID and CFTOKEN values, all we need to do is copy them from the Cookie scope. The SessionID value is then a combination of the application name, the CFID, and the CFToken.
After we've done this, the user is redirected back to the main display page, which gives the following output
As you can see, the Session scope has been repopulated. Keep in mind, however, that this does not mean that the user's session has been restarted or that this is a new session in any way. Remember, the ColdFusion session is powered by the relationship established by the cookies being passed back by the browser. If we truly want to "end" the current session (so to speak), we have to stop the browser from reposting those cookies. To do this, we have to expire the current CFID and CFTOKEN cookies. By clicking on the "Expire Cookies" link, we get taken to this page:
<!--- Loop over all the cookies and expire them. In reality, we only need to expire the CFID and CFTOKEN session identifiers, but for our purposes, clearing all the cookies is sufficient. ---> <cfloop item="name" collection="#cookie#"> <!--- Have them expire immediately. This way, when the response is sent to the browser, all of their cookies will be cleared. ---> <cfcookie name="#name#" value="" expires="now" /> </cfloop> <!--- Redirect back to index page. ---> <cflocation url="index.cfm" addtoken="false" />
As you can see here, we are simply looping over all of the current cookies (that ColdFusion knows about) and setting them to expire immediately. When these expiration requests get flushed to the browser with the current response, it will prevent the browser from reposting the CFID and CFTOKEN values. In doing so, on the subsequent request to the main display page, the ColdFusion server will think that the user is a new user and provide them with a new session. This will give us the following output:
As you can, ColdFusion recognized the request for the display page as coming from a new user and therefore generated new CFID and CFTOKEN values (from 17603 to 17604). While this created a new session, please keep in mind that the previous session is still technically "running;" meaning, it has neither timed out nor has the onSessionEnd() event handler been invoked. The pervious session will only really end once it has been inactive for the SessionTimeout duration as defined by the Application.cfc.
Now, while calling structClear() on the ColdFusion Session scope doesn't end the user's session, it does have a place. If you are going to try and end the user's session prematurely (and I don't mean that negatively), calling structClear() on the Session has a value in that it will trigger garbage collection on the Session-scoped values. So, while the session itself is still active, ColdFusion will be able to free up the memory space more immediately.
It should be noted that all of this pertains to standard ColdFusion session management. If you enable J2EE sessions, there are completely different rules, most of which I cannot speak to from experience.
ColdFusion applications are very interesting at the mechanical level and can be a bit confusing. If you were confused at all before, I hope that this exploration has brought some level of clarity to the matter.
Want to use code from this post? Check out the license.
I remember seeing some hidden functions that would expire sessions properly.
Basically either session.setMaxInactiveInterval(1) or session.invalidate()
Haven't tested in CF9 though but it works in CF7/8.
Also remember that, depending on the cookie scope, you may have to clear several sets of cookies:
The session invalidate stuff looks very interesting. I actually just saw that for the first time the other day. After a little research, it looks like this:
... only works with J2EE sessions, not traditional ColdFusion sessions:
Now, ironically, the LiveDocs say that structClear() is typically good enough to clear a session; but this has definitely not been my experience.
What was CFMAGIC?
Awesome explanation. From a security stand point, doing both of these, I think , would be a good idea. Clear the previous session scope and then expire the cookies and move on to the next one.
This way, if the previous session (which as you said, does live on until expiration) does become compromised, at least it will be empty and is not being actively used, so is essentially worthless.
Ah, good point; I had not even considered it from a security point of view.
Just checked and .setMaxInactiveInterval(1) works quite well. I've just altered my cftracker riaforge project and added the ability to nuke any session. Great fun! Now to place it on the shared development server in work and watch the other developers scratch their heads when their sessions go missing. Mwuahaha.
Ha ha ha, cool, I'll check it out :)
@Ben haven't released it yet, having fun with it first ;)
Now I have http://wp.me/py3ue-cu >:)
Great fun but I can't think of a way to use this power for good...
Ok, now I'll check it out ;)
I think this is a bit misleading, because your Expire.cfm isn't really expiring the user's session, it's just forcing the server to re-assign them new cookies, thus creating a *new* session. The old session is still in-memory and technically active, it's just no longer being used.
I tried to explain that as best as I could in the blog post:
While this created a new session, please keep in mind that the previous session is still technically "running;" meaning, it has neither timed out nor has the onSessionEnd() event handler been invoked.
Ben, it turns out this is a problem for other languages, too. I occasionally have to do some PHP development and we ran into the same issue there. I hope that we as a community can develop an acceptable solution on killing authenticated sessions without using cflogin, etc.
Thanks for putting this out in the community. I'm curious what Jason Dean comes up with regarding dealing with sessions and security.
Our company doesn't use the session scope, but uses the client scope and I am thinking we might be able to extend the functions of the database store.
BTW: Noticed you were using cflocation with a relative url instead of a absolute url. Although most popular browsers will work with relative urls, the standard is for a absolute url, and some older browsers and other protocol-strict http clients will fail with a relative url, just thought I would point it out.
Have you tested setMaxInactiveInterval(1) with CF Sessions? Because my understanding is that it will only work with JEE sessions.
@ben and @dan
Maybe it would make it more clear to call it expireCookie.cfm. Because that is actually what's happening.
It's an interesting problem to be sure. I'll probably do some more exploration of this.
That's interesting - I have not heard that before (not saying it is wrong). I've always used relative paths. Can you point me to any resources on this?
CFMAGIC is ColdFusion's way of reminding itself that it's set domain-level cookies.
Ahh, that makes sense - it's practically self-documenting ;)
If you look up the docs on the location http header(which cflocation is doing) it is specified, the php docs are more explicit about it as well as lots of varies rewrite engine docs. Its one of those things that most browsers are forgiving about so usually it doesn't matter, but it is something to be aware of.
http://www.ietf.org/rfc/rfc2616.txt RFC 2616, section 14.30
Does anyone know what CF does internally based on the session expiration time configured either with the admin or at the application.cfc level? Does it garbage collect?
Great information and explanation. Thanks Ben.
CFTracker is very cool. I just downloaded it and was playing around. You have definitely figured out how to do something that I have been trying to do for a long time. It seems that you can, in fact, expire ColdFusion sessions with setMaxInactiveInterval(1).
That is awesome. Thanks!
Glad you like it. It's just a little utility cfc for stats and information, but the little summary tool does come in handy. Plus the setMaxInactiveInterval is great. With CF8+ you can also read information from other sessions, it'd be easy enough to pull out a username from a session to more easily identify them. That way you'd know who you were marking for expiration ;)
Hmm, I am not sure if I like the idea of having to use the absolute path; feels like it requires more of a directive than is necessary.
Have you ever encountered a browser that does not honor relative URL redirects? I have not (that I can think of).
Great exploration of this topic. Thank you. The comments are well worth the reading also.
I didn't read all the comments (I may do this later) but I would like to comment this a bit.
Where did you read that structClear() should be enought to clear a session? The interessting part is, that this looks like a function wich has to work like this. In older version of CF the structClear() function cleared the CFTOKEN and the CFID, too. http://kb2.adobe.com/cps/174/tn_17479.html
In my opinion this "new" behaviour makes absolute sense. Session and cookies are two different kind of, let us call it "user defined data", wich should not really belong together. So, we're saving some data in cookies, and some data in the session. CF also stores your CFTOKEN and your CFID in your session (okay thats something they could change.. for better understanding). But if you're clearing your session vars it doesn't mean you also wanna clear your cookies IMO. It could be very useful that a structClear() of a session doesnt also clear your cookies.
I agree with you that this behavior does make sense for all the same reasons; as you put it, the Session scope is just another place to store data - it's not the primary connection.
Here is a follow-up post on various approaches to actually killing the ColdFusion session (building on this post and the comments):
I find this very interesting I am running on Coldfusion 8 server and have copied your code and tried this out.
I can clear the session varibles but when I go to clear the cookies.
It still has the same CFID.
So I though I should try again.
I cleared the browser cookies and tried again in a new browser and the CFID is still there...
What am I doing wrong? Is there a server check I need on. I am getting the same results from my other application I have created and it is driving me crazy.
Thanks in advance,