The other day, I was working on a login system that had a "Remember Me" checkbox where, when checked, the user would be automatically logged back into the system upon subsequent visits to the site. I have built systems like this many times before, but for some reasons, I was hitting a mental road block; I was having trouble wrapping my head around certain parts of it, I think because the login system I was working on at the time was a little unique. I tried searching Google, but didn't really find any great examples.
Since I didn't see anything great out there, I figured I would sit down and try to write up a quick little demo application that explains one way of creating a "Remember Me" login system. I say "one way" because there are several ways to do this and different ways are more secure depending on your needs. For the majority of us out there, I would bet that when tracking login status, we are using something like a boolean flag or the existence of a user ID. So, to keep it simple, that is what I am going to do - for the following demo application, logged-in status will be determined by the existence of a SESSION-based user ID (and since I am not working with a database, that ID will be either 1 [logged in] or 0 [logged out]).
Before we look at the code, let's just think for a moment about what happens in our application when we have automatic logins. To the point, when do we want to check to see if the user is automatically logged in? This is the part that was tripping me up the other day for some reason. There's only one place in the application that we ever want to try to automatically log a user in - when their session starts. In ColdFusion, this can be hooked into use Application.cfc's OnSessionStart() event method.
Think about it - when does the session start? When the user first enters the application. And, when do we want to "remember" them? When they first show up. Once a user is already in the application, they have either logged in, or are in a logged-out status by choice. There is no need to worry about logging a user in automatically past the first page since the user's status will be determined by their actions at that point.
But what about logout? Another concept to think about - a user could logout and still be in the application past the first page visit. This is absolutely true (and is of all logout actions); however, if the user chooses to logout, should we be worrying about logging them back in automatically? Of course not; that would go against the whole gesture of logging out. So, again, we see that we only ever have to worry about the very first visit to the application, when the session starts.
That being said, let's take a look at a quick demo application. And, let's start with the Application.cfc which is where we will have hooks into the session startup event. In the Application.cfc, there's really two things to notice: first, in the OnRequest() event method, we are forcing the login page if the user is not logged in. And, secondly, we are checking for an automatic login in the OnSessionStart() event method:
Launch code in new window » Download code as text file »
In the OnSessionStart() we are wrapping the whole cookie / automatic login system in a CFTry / CFCatch block. This is because we are working with third-party data that may not even exist. There are a lot of chances here for the code to throw an exception:
Instead of worrying about each of those conditions, I am just putting it all into a CFTry / CFCatch. That way, if an exception get's thrown, I know the value either did not exist or was not valid, and in either case, we don't want to log the user in automatically. And, since this only happens once per session, the cost of exception creation is non-existent.
In the OnRequest() event method, simply take note that if the user's ID does not have non-zero value, I am forcing the login page to be rendered regardless of what template was requested.
Now, let's take a look at the login page. I am trying to keep this simple, so I am not worrying about form errors or anything like that. In fact, this whole application lacks error handling - I am trying to keep the signal to noise ratio very high.
Launch code in new window » Download code as text file »
If the user logs into the system and has selected the "Remember Me" option, then we create the obfuscated and encrypted value and store that as a cookie that never expires. We are doing so much to the ID value since it does pose a security risk. Again, this is not the most complex example of how this functionality can be built. But, by altering the value of the stored ID, we are at least making it extremely difficult for users to go in and mess with the value in an attempt to login as a different user.
When the user logs out, the logic is simple:
Launch code in new window » Download code as text file »
All we have to do is clear the session flag and erase the cookie.
Then, I just had an index page to make sure the application was working properly:
Launch code in new window » Download code as text file »
That's all there is to it. Again, there are more secure ways of doing this and better ways of building login systems in general. But, the ideas will be the same and the places in which we run our logical checks will also be the same.
Download Code Snippet ZIP File
Comments (10) | Post Comment | Ask Ben | Permalink | Other Searches | Print Page
New ColdFusion CFMailParam "Remove" Attribute Makes Deleting Attachments Simple
Image Manipulation ColdFusion Wrapper Component
Nice encryption key. How about... $3xyr3@dh3@d$
Posted by CV on Apr 29, 2008 at 4:57 PM
@CV,
Niiice :)
Posted by Ben Nadel on Apr 29, 2008 at 5:33 PM
you lost me at: "Store the CF id and token. We are about to clear the
session scope for intialization and want to make sure
we don't lose our auto-generated tokens."
Why do use use LOCAL again?
Posted by Henry on Apr 30, 2008 at 12:37 PM
@Henry,
LOCAL is a "var"ed variable that created a pseudo local scope for the function. It's really just a short hand. For example,
<cfset var a = 1 />
<cfset var b = 2 />
... is the same as:
<cfset var LOCAL = {
a = 1,
b = 2
} />
I am just focusing my "var" to one variable, and then housing other local variables inside of it.
As far as the clearing of the CFID and CFTOKEN, the OnSessionStart() can be launched manually from the OnRequestStart() method depending on URL parameters. Therefore, we need to clear the SESSION if we are manually resetting the data. Of course, we don't want to lose the CFID / CFTOKEN values as these are part of the core ColdFusion session management framework. So, we store them temporarily, clear the SESSION, and then "recreate" them.
Posted by Ben Nadel on Apr 30, 2008 at 12:44 PM
Thanks.
I wonder if there's a cleaner implementation of Remember Me.
Posted by Henry on Apr 30, 2008 at 1:07 PM
@Henry,
Cleaner in what way?
Posted by Ben Nadel on Apr 30, 2008 at 1:19 PM
@Ben,
Although the example works wonderfully, it is not the easiest example to follow because of code length. My brain ran out of ram the first time reading your code.
Oh well, the verboseness of CFML is not your fault. :)
Maybe I should have copy and paste the code into CFEclipse then read the code. It's hard to read so much CFML in a browser.
Posted by Henry on Apr 30, 2008 at 1:33 PM
@Henry,
Most definitely. When reading it in a blog post, not only is there a lot of peripheral data (the whole site), there is also a mixture of code and explanation that intertwine. Certainly a lot to take in, especially when there are multiple code files in question.
I don't know if this would be different in any language as there really isn't a lot going on here when you boil it down. It's just a matter of getting comfortable with all the different moving parts.
Posted by Ben Nadel on Apr 30, 2008 at 1:43 PM
I like that you're including salt with the encrypted rememberMe cookie (the CreateUUID stuff), that's a good way to keep nosy people out of there, and drive up the length of the encrypted text to something not so easily brute-forceable.
It's not as portable, but I also like to include a unique (eg, hash(CreateUUID()) )token on the account. In that way, even if the encryption key is cracked (which might not be too hard with cfmx_compat), you still can't spoof another user.
Sometimes I like to include an encoded expiry too, such that perhaps I only honor a given RememberMe cookie for a certain number of weeks so that even if the cookie is captured, eventually it expires anyway.
So for example: "#CreateUUID()#:#SESSION.User.Id#:#SESSION.User.Token#:#DateFormat(now(), 'YYYY-MM-DD')#:#CreateUUID()#"
Posted by Eric on May 4, 2008 at 9:01 AM
@Eric,
I like the idea of including the Hash() as well to compare the unecrypted value to. Very slick. That hadn't even occurred to me. But you're right - even if someone did crack the encryption (and I'm told that cfmx_compat is nothing special), it would make is extremely hard to spoof a different value.
Posted by Ben Nadel on May 5, 2008 at 1:46 PM