Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Mike Collins

Ask Ben: Creating Single Location Logins

By Ben Nadel on

Ben, I was hoping you could point us in the right direction. We are creating an application that can be accessed from the public and as a security precaution we want to make sure that one account can only be used from one location at a time. This way, if I sign on using one terminal, I know that any left-open sessions on another terminal will automatically be disabled. I've done logins before, but not sure how to go about this. Thanks in advance!!!

When creating a single-location login style application, the key is to have a way for each session to announce itself as the currently "Active" session for a particular set of credentials. This way, when a user logs in using one computer, their new session will announce itself as the rightful session and any existing sessions (for the same set of credentials) will detect this, see that they are no longer valid sessions, and automatically logout (or however else you may want to handle this).

 
 
 
 
 
 
 
 
 
 

Because the clients (browsers) are distributed on the internet, the data regarding which sessions are the currently active ones must be centralized around the application. This might be in a database table or some other form of cached memory structure. The larger your application, the more likely you will need to store this type of information in a database so as not to run out of RAM. However, since this is just a demo of the single-location sign-on principle and I know that RAM is not an issue, I will be caching this data in the APPLICATION scope.

Now, in order for this concept to work, every session related to a single set of credentials needs a way to identify itself both as part of this one group and also as unique, individual session. To do this, I am using a "User ID" (unique identifier for a user record with the given credentials) to index the group and a UUID (universally unique identifier) to index each individual session.

Once we have these data values in place, we simply need to enforce a relationship between the active session's UUID and the cached group ID for the given credentials. And, since a new session might begin at any moment, this relationship has to be checked for each page request. Because this check needs to happen so often, I prefer using cached data structures rather than database calls for as long as is practical.

To get a better understanding of this, let's take a look at our sample Application.cfc. Notice that in the APPLICATION scope, we have a SessionKeys cache which is indexed using the user IDs:

  • <cfcomponent
  • output="false"
  • hint="I provide application level event handlers and settings.">
  •  
  • <!--- Define the application settings. --->
  • <cfset THIS.Name = "SingleLocationSessionDemo" />
  • <cfset THIS.ApplicationTimeout = CreateTimeSpan( 0, 0, 5, 0 ) />
  • <cfset THIS.SessionManagement = true />
  • <cfset THIS.SessionTimeout = CreateTimeSpan( 0, 0, 5, 0 ) />
  •  
  •  
  • <cffunction
  • name="OnApplicationStart"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="I run when the application is being (re)initialized.">
  •  
  • <!---
  • Clear the application scope (in case we are
  • re-initializing rather than booting up for the
  • first time).
  • --->
  • <cfset StructClear( APPLICATION ) />
  •  
  • <!---
  • Create a structure to hold onto the unique session
  • keys (based on user ID). For this demo, I am caching
  • in-memory, but in a large app, this would probably
  • be a database table.
  • --->
  • <cfset APPLICATION.SessionKeys = {} />
  •  
  • <!--- Return continue-loading flag. --->
  • <cfreturn true />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="OnSessionStart"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I run when the session is being (re)initialized.">
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = {} />
  •  
  • <!--- Cache the ID/Token so that we don't lose them. --->
  • <cfset LOCAL.CFID = SESSION.CFID />
  • <cfset LOCAL.CFTOKEN = SESSION.CFTOKEN />
  •  
  • <!---
  • Clear the session scope (in case we are
  • re-initializing rather than booting up for the
  • first time).
  • --->
  • <cfset StructClear( SESSION ) />
  •  
  • <!---
  • Re-set the ID/Token values to maintain session
  • hooks properly.
  • --->
  • <cfset SESSION.CFID = LOCAL.CFID />
  • <cfset SESSION.CFTOKEN = LOCAL.CFTOKEN />
  •  
  •  
  • <!--- Param the default values of the user. --->
  • <cfset SESSION.User = {
  • ID = 0,
  • SessionID = "",
  • LoggedIn = false
  • } />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="OnRequestStart"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="I run when the request is being initialized.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Page"
  • type="string"
  • required="true"
  • hint="I am the template being requested."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = {} />
  •  
  • <!---
  • Check to see if the application is being reset
  • manually with a URL flag.
  • --->
  • <cfif StructKeyExists( URL, "reset" )>
  •  
  • <!--- Manually reset app and session. --->
  • <cfset OnApplicationStart() />
  • <cfset OnSessionStart() />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Check to see if this user is logged in at all. We
  • only care about checking the single-location session
  • if they are logged-in.
  • --->
  • <cfif SESSION.User.LoggedIn>
  •  
  • <!---
  • Now that we know this user is logged-in, we need
  • to check to see if their login is still valid.
  • Because we are reading / writing to this share
  • data in multiple places, this feels like it might
  • be an appropriate place for a lock. To limit the
  • bottle-necking affects of the lock, I am making
  • it session-id-based.
  • --->
  • <cflock
  • name="login-check-#SESSION.User.ID#"
  • type="exclusive"
  • timeout="5">
  •  
  • <!---
  • Check to see if the current session is the
  • ACTIVE session location for this user. In
  • order for that to be true, the session-cached
  • ID must be the same as that cached in our
  • APPLICATION.
  • --->
  • <cfif (
  • (NOT StructKeyExists( APPLICATION.SessionKeys, SESSION.User.ID )) OR
  • (APPLICATION.SessionKeys[ SESSION.User.ID ] NEQ SESSION.User.SessionID)
  • )>
  •  
  • <!---
  • Either the user's session ID has not been
  • cached in the APPLICATION yet, OR this
  • same user has signed on in a different
  • location, rendering the current session
  • as NO LONGER VALID.
  •  
  • Make sure to flag this user as being no
  • longer logged-in. In our simple demo,
  • this is denoted by the user ID and
  • logged-in flag.
  • --->
  • <cfset SESSION.User.ID = 0 />
  • <cfset SESSION.user.LoggedIn = false />
  •  
  • </cfif>
  •  
  • </cflock>
  •  
  • </cfif>
  •  
  • <!--- Return continue-loading flag. --->
  • <cfreturn true />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="OnRequest"
  • access="public"
  • returntype="void"
  • output="true"
  • hint="I execute the appropriate page template.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Page"
  • type="string"
  • required="true"
  • hint="I am the template being requested."
  • />
  •  
  • <!---
  • Check to see if this user is logged-in. If so, then
  • include the requested template. If not, then include
  • the login page (unless of course the user is already
  • requesting the login-style pages (including the login
  • processing page).
  • --->
  • <cfif (
  • SESSION.User.LoggedIn OR
  • REFind( "login(_process)?\.cfm$", CGI.script_name )
  • )>
  •  
  • <!--- Include requested page. --->
  • <cfinclude template="#ARGUMENTS.Page#" />
  •  
  • <cfelse>
  •  
  • <!---
  • The user is not logged-in. So, no matter what
  • template they requested, force them to view the
  • login page.
  • --->
  • <cfinclude template="login.cfm" />
  •  
  • </cfif>
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="OnSessionEnd"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I run when the session is being ended.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Application"
  • type="struct"
  • required="true"
  • hint="I am the application scope used by the session."
  • />
  •  
  • <cfargument
  • name="Session"
  • type="struct"
  • required="true"
  • hint="I am the session scope used by the session."
  • />
  •  
  • <!---
  • Clear out the session ID tracking from the
  • application. This will make sure that the session
  • ends at all locations.
  • --->
  • <cfset StructDelete(
  • APPLICATION.SessionKeys,
  • ARGUMENTS.Session.User.ID
  • ) />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  • </cfcomponent>

The magic here really happens in the OnRequestStart() application event method. At the start of every page request, we are checking two very important facts related to the current session:

  1. Is my user ID indexed in the application?
  2. Does the UUID cached at my user ID match my session's UUID?

If either one of these facts is not true, then the current session is not the currently active session. If the user ID has not been cached yet, then it means that this user is either not yet logged in or that a parallel session with the same user ID has explicitly logged out. In either case, the current session must not be considered logged in. If, however, the user ID is indexed in the key cache, but the cached UUID does not match the current session's UUID, it must mean that a parallel session has just logged in using the same credentials and has overwritten the current session's UUID. In that case, the current session must consider itself invalid (and must automatically logout in our scenario).

I am using a CFLock tag around the checking of the SessionKeys cache. In this demo, I would say that CFLock here is definitely not necessary; however, the more complicated the process gets for logging users in and out, the more likely you might want to put locking around this subroutine. Of course, it could easily be argued that since the cached is only ever updated when logging in and logging out, no real corruption or race conditions are possible. Regardless, to help fight against creating a bottle-neck, I am using an ID-based naming convention for the lock. This way, one user's security checks should not slow down another user's security checks.

Now that we see where the handshake / agreement has to occur at the start of every page request, let's take a look at where the SessionKeys cache is actually updated. First, let's take a look at the login processing page. Since this is just a demo, the user ID is hard coded, but the theory is the same:

  • <!---
  • Because this is just a demo, we are going to hardcode the
  • user ID so that all demo users are the same user. The session
  • key will be auto-generated for us.
  • --->
  • <cfset SESSION.User.ID = 4 />
  •  
  • <!---
  • Create a unique ID to make sure there is no chance that
  • another location instance of this user will have the same
  • session ID.
  • --->
  • <cfset SESSION.User.SessionID = CreateUUID() />
  •  
  • <!--- Flag user as logged-in. --->
  • <cfset SESSION.User.LoggedIn = true />
  •  
  •  
  • <!---
  • We need to update the session-cache using the user ID and
  • the new session ID.
  •  
  • NOTE: Because this is a part of the application where race
  • conditions might actually apply, I am going to lock this
  • area. To make this less of a bottle-neck in the system, I am
  • going to make the name of the lock based on the session ID.
  • --->
  • <cflock
  • name="login-check-#SESSION.User.ID#"
  • type="exclusive"
  • timeout="5">
  •  
  • <!--- Cache this single-location session ID. --->
  • <cfset APPLICATION.SessionKeys[ SESSION.User.ID ] = SESSION.User.SessionID />
  •  
  • </cflock>
  •  
  •  
  • <!--- Redirect to home page. --->
  • <cflocation
  • url="./index.cfm"
  • addtoken="false"
  • />

As you can see, each logged-in session gets is own UUID. When a user logs in, we take the UUID for the current session and cache it in the SessionKeys. This will make sure the current session location is deemed as the only valid one in the context of other sessions previously logged-in using the same credentials.

Likewise, as a precaution, when a user logs out, we delete the user ID index from the SessionKeys cache to indicate that no session using these credentials can be considered valid any longer:

  • <!---
  • We need to remove the session key from the APPLICATION cache
  • so that this user is fully logged out in every location. Now,
  • again, I don't really think locking here is necessary, but
  • the more complex the inner workings of this, the more it may
  • be worth while.
  • --->
  • <cflock
  • name="login-check-#SESSION.User.ID#"
  • type="exclusive"
  • timeout="5">
  •  
  • <!---
  • Reset the user and erase it from the application's
  • session cache.
  • --->
  • <cfset StructDelete(
  • APPLICATION.SessionKeys,
  • SESSION.User.ID
  • ) />
  •  
  • </cflock>
  •  
  •  
  • <!--- Reset the user. --->
  • <cfset SESSION.User.ID = 0 />
  • <cfset SESSION.User.LoggedIn = false />
  •  
  • <!--- Redirect to homepage. --->
  • <cflocation
  • url="./index.cfm"
  • addtoken="false"
  • />

There's a lot more that can go into this type of security including the way in which conflicting sessions are handled; but, I hope that this can at least give you some ideas and help point you in the right direction.

Tweet This Great article by @BenNadel - Ask Ben: Creating Single Location Logins Thanks my man — you rock the party that rocks the body!


Reader Comments

Hey Ben... bear with me here till the end of my comment, eh?

Why not just have your login code store the user name for every successful login in the application scope. They've logged in on that machine, in that browser... so if they try to log in again, it's got to be from somewhere else, period. From there it's a simple matter of using onSessionEnd() in tandem with the logout code to remove them from the application scope.

This could be accomplished with less than 15 lines of additional code if you already have a working login/logout mechanism. If you're using the session scope to store your login marker, then you're not logged out till your session times out or you log out, so checking login validity on every request is redundant. With onSessionEnd() you have the ability to toggle session.loggedIn AND remove them from the login tracker in the application scope.

So, I guess what I'm really getting at is this: Either you have some really good reason for making this so complicated that I'm missing and really need you to explain to me (because that happens more often than I like to admit) or you've grossly overcomplicated a very simple solution to a really common problem... if it's the former I really do want to find out what I'm missing, and if it's the latter then I really have to ask:

Why, dude??? Why?? :)

Reply to this Comment

@Jared,

There is a good chance I am grossly over complicating it - I wouldn't put that past me :) But, after reading your comment, I am not sure if we would be accomplishing the same thing. My primary concern is that only one session per credential can be active at a time. I am not concerned with where that new session is coming from.

So, if I got the gist of your comment, here is what I think the difference is:

* You ONLY allow one session until it is explicitly logged out or times out.

* I ONLY allow one session at a time, but no particular session has precedence other than on a purely last-come basis.

Here is the scenario I am playing out in my mind that I think my scenario works to protect:

I use GMail at home, but since I live alone, I tend to just leave the FireFox window open with my GMail logged in. However, I totally forgot that the exterminator is coming today (while I'm at work). Being the nervous nelly that I am, I quickly log into my GMail account from my work computer knowing that this login-action will automatically logout (or disable) my GMail account window at home (thereby preventing the exterminator from being able to snoop through my always-logged-in GMail account at home).

Does that make more sense?

Reply to this Comment

Not related to the topic at all, but did you know that GMail will show you other active sessions and allow you to log them out? Logging in from a different place does not automatically log out the other session, you have to explicitly do it.

Reply to this Comment

Ahh, OK. So I was missing part of the picture... I hadn't read the code closely enough. Sorry. Yeah, that makes a certain amount of sense... beacuse you have to associate usernames with session IDs of some sort (J2EE or otherwise), you have to have more than just a user id cached in the application scope.

As I'm mentally walking thru the process I can see about 4 different ways to accomplish this (accounting for the location-based login) but only one of them really reduces the code... and that's to store something like:

application.sessionStore[userID] = session.urlToken

That simplifies some things... and the cfif could be cut back to

<cfif structKeyExists(application.sessionStore,userID) and application.sessionStore.userID NEQ session.urlToken>

So the reduction is more in the statements themselves, rather than, as before, the structure of the solution. Other than the crazy things you're doing to your session scope ;) I guess I have to say decent job dude. :)

Reply to this Comment

@Todd,

That is bitchy!!! That is exactly what I was envisioning as I was writing this out (although they have the manual sign out vs. the implicit). That is way cool. I had no idea that was there. Thanks for pointing that out.

@Jared,

Thanks man. Your concept was good as well, just solving a slightly different problem.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.