Creating A "Remember Me" Login System In ColdFusion

Posted April 29, 2008 at 9:59 AM

Tags: ColdFusion

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 »

  • <cfcomponent
  • output="false"
  • hint="I define the application and root-level event handlers.">
  •  
  • <!--- Define application settings. --->
  • <cfset THIS.Name = "RememberMeDemo" />
  • <cfset THIS.ApplicationTimeout = CreateTimeSpan( 0, 0, 5, 0 ) />
  • <cfset THIS.SessionManagement = true />
  • <cfset THIS.SessionTimeout = CreateTimeSpan( 0, 0, 0, 20 ) />
  • <cfset THIS.SetClientCookies = true />
  •  
  • <!--- Define the request settings. --->
  • <cfsetting
  • showdebugoutput="false"
  • requesttimeout="10"
  • />
  •  
  •  
  • <cffunction
  • name="OnApplicationStart"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="I run when the application boots up. If I return false, the application initialization will hault.">
  •  
  • <!---
  • Let's create an encryption key that will be used to
  • encrypt and decrypt values throughout the system.
  • --->
  • <cfset APPLICATION.EncryptionKey = "S3xy8run3tt3s" />
  •  
  • <cfreturn true />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="OnSessionStart"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I run when a session boots up.">
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = {} />
  •  
  • <!---
  • 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.
  • --->
  • <cfset LOCAL.CFID = SESSION.CFID />
  • <cfset LOCAL.CFTOKEN = SESSION.CFTOKEN />
  •  
  • <!--- Clear the session. --->
  • <cfset StructClear( SESSION ) />
  •  
  • <!---
  • Replace the id and token so that the ColdFusion
  • application knows who we are.
  • --->
  • <cfset SESSION.CFID = LOCAL.CFID />
  • <cfset SESSION.CFTOKEN = LOCAL.CFTOKEN />
  •  
  •  
  • <!--- Create the default user. --->
  • <cfset SESSION.User = {
  • ID = 0,
  • DateCreated = Now()
  • } />
  •  
  •  
  • <!---
  • Now that we are starting a new session, let's check
  • to see if this user want to be automatically logged
  • in using their cookies.
  •  
  • Since we don't know if the user has this "remember me"
  • cookie in place, I would normally say let's param it
  • and then use it. However, since this process involves
  • decryption which might throw an error, I say, let's
  • just wrap the whole thing in a TRY / CATCH and that
  • way we don't have to worry about the multiple checks.
  • --->
  • <cftry>
  •  
  • <!--- Decrypt out remember me cookie. --->
  • <cfset LOCAL.RememberMe = Decrypt(
  • COOKIE.RememberMe,
  • APPLICATION.EncryptionKey,
  • "cfmx_compat",
  • "hex"
  • ) />
  •  
  • <!---
  • For security purposes, we tried to obfuscate the
  • way the ID was stored. We wrapped it in the middle
  • of list. Extract it from the list.
  • --->
  • <cfset LOCAL.RememberMe = ListGetAt(
  • LOCAL.RememberMe,
  • 2,
  • ":"
  • ) />
  •  
  • <!---
  • Check to make sure this value is numeric,
  • otherwise, it was not a valid value.
  • --->
  • <cfif IsNumeric( LOCAL.RememberMe )>
  •  
  • <!---
  • We have successfully retreived the "remember
  • me" ID from the user's cookie. Now, store
  • that ID into the session as that is how we
  • are tracking the logged-in status.
  • --->
  • <cfset SESSION.User.ID = LOCAL.RememberMe />
  •  
  • </cfif>
  •  
  • <!--- Catch any errors. --->
  • <cfcatch>
  • <!---
  • There was either no remember me cookie, or
  • the cookie was not valid for decryption. Let
  • the user proceed as NOT LOGGED IN.
  • --->
  • </cfcatch>
  • </cftry>
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="OnRequestStart"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="I perform pre page processing. If I return false, I hault the rest of the page from processing.">
  •  
  • <!--- Check for initialization. --->
  • <cfif StructKeyExists( URL, "reset" )>
  •  
  • <!--- Reset application and session. --->
  • <cfset THIS.OnApplicationStart() />
  • <cfset THIS.OnSessionStart() />
  •  
  • </cfif>
  •  
  • <!--- Return out. --->
  • <cfreturn true />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="OnRequest"
  • access="public"
  • returntype="void"
  • output="true"
  • hint="I execute the primary template.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Page"
  • type="string"
  • required="true"
  • hint="The page template requested by the user."
  • />
  •  
  • <!---
  • We are going to be using the user's ID as a the way
  • to check for logged-in status. Check to see if the
  • user is logged in based on the ID. If they are, then
  • include the requested page; if they are not, then
  • force the login page.
  • --->
  • <cfif SESSION.User.ID>
  •  
  • <!--- User logged in. Allow page request. --->
  • <cfinclude template="#ARGUMENTS.Page#" />
  •  
  • <cfelse>
  •  
  • <!---
  • User is not logged in - include the login page
  • regardless of what was requested.
  • --->
  • <cfinclude template="login.cfm" />
  •  
  • </cfif>
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  • </cfcomponent>

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:

  • RememberMe doesn't exist in the COOKIE scope.
  • We try to decrypt an empty string.
  • The decrypted string does not have a second list value.

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 »

  • <!--- Param the form values. --->
  • <cfparam name="FORM.username" type="string" default="" />
  • <cfparam name="FORM.password" type="string" default="" />
  • <cfparam name="FORM.remember_me" type="boolean" default="false" />
  •  
  • <!---
  • Check to see if the form has been submitted. Since we
  • are trying to keep this low-level, just check to see
  • if both values match up.
  • --->
  • <cfif (
  • (FORM.username EQ "big") AND
  • (FORM.password EQ "sexy")
  • )>
  •  
  • <!---
  • The user has logged in. This is where we would do the
  • authorization; however, since we are just running a very
  • simple demo app, simply give the user an ID of "1" to
  • signify that they have logged in.
  • --->
  • <cfset SESSION.User.ID = 1 />
  •  
  •  
  • <!--- Check to see if the user want to be remembered. --->
  • <cfif FORM.remember_me>
  •  
  • <!---
  • The user wants their login to be remembered such that
  • they do not have to log into the system upon future
  • returns. To do this, let's store and obfuscated and
  • encrypted verion of their user ID in their cookies.
  • We are hiding the value so that it cannot be easily
  • tampered with and the user cannot try to login as a
  • different user by changing thier cookie value.
  • --->
  •  
  • <!---
  • Build the obfuscated value. This will be a list in
  • which the user ID is the middle value.
  • --->
  • <cfset strRememberMe = (
  • CreateUUID() & ":" &
  • SESSION.User.ID & ":" &
  • CreateUUID()
  • ) />
  •  
  • <!--- Encrypt the value. --->
  • <cfset strRememberMe = Encrypt(
  • strRememberMe,
  • APPLICATION.EncryptionKey,
  • "cfmx_compat",
  • "hex"
  • ) />
  •  
  • <!--- Store the cookie such that it never expires. --->
  • <cfcookie
  • name="RememberMe"
  • value="#strRememberMe#"
  • expires="never"
  • />
  •  
  • </cfif>
  •  
  •  
  • <!--- Redirect to root. --->
  • <cflocation
  • url="./"
  • addtoken="false"
  • />
  •  
  • </cfif>
  •  
  •  
  • <cfoutput>
  •  
  • <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  • <html>
  • <head>
  • <title>Login</title>
  • </head>
  • <body>
  •  
  • <h1>
  • Application Login
  • </h1>
  •  
  • <form action="#CGI.script_name#" method="post">
  •  
  • <label>
  • Username:
  • <input type="text" name="username" size="20" />
  • </label>
  • <br />
  • <br />
  •  
  • <label>
  • Password:
  • <input type="password" name="password" size="20" />
  • </label>
  • <br />
  • <br />
  •  
  • <label>
  • <input type="checkbox" name="remember_me" value="1" />
  • Remember Me
  • </label>
  • <br />
  • <br />
  •  
  • <input type="submit" value="Login" />
  •  
  • </form>
  •  
  • </body>
  • </html>
  •  
  • </cfoutput>

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 »

  • <!---
  • When logging out, we want to both log out the current user
  • AND make sure that they don't get automatically logged in
  • next time.
  • --->
  •  
  • <!---
  • Since our application is using the User ID to keep track
  • of login status, let's reset that value.
  • --->
  • <cfset SESSION.User.ID = 0 />
  •  
  • <!---
  • We also don't want the user to be automatically logged
  • in again, so remove the client cookies.
  • --->
  • <cfcookie
  • name="RememberMe"
  • value=""
  • expires="now"
  • />
  •  
  •  
  • <!--- Now that the user has been logged out, redirect. --->
  • <cflocation
  • url="./"
  • addtoken="false"
  • />

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 »

  • <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  • <html>
  • <head>
  • <title>Remember Me Login Demo</title>
  • </head>
  • <body>
  •  
  • <h1>
  • Remember Me Login Demo
  • </h1>
  •  
  • <p>
  • Welcome to the "Remember Me" login demo application.
  • If you are seeing this page, then you have successfully
  • logged into the system (or were logged in automatically
  • using your cookies).
  • </p>
  •  
  • <p>
  • <a href="logout.cfm">Logout</a>
  • </p>
  •  
  • </body>
  • </html>

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



Keep your Web site content fresh and your overhead costs low with Savvy Content Manager

Reader Comments

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


Post Comment  |  Ask Ben


Home   |   Web Log   |   ColdFusion   |   Projects   |   Resume   |   Job Form   |   Search   |   Contact
Epicenter Consulting - Custom Software Solutions for Business Evolution HostMySite.com - The Leader In ColdFusion Hosting