Skip to main content
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Steve Withington
Ben Nadel at cf.Objective() 2009 (Minneapolis, MN) with: Steve Withington

Ask Ben: Creating Single Location Logins

By
Published in , Comments (5)

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.

Want to use code from this post? Check out the license.

Reader Comments

3 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?? :)

15,912 Comments

@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?

48 Comments

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.

3 Comments

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. :)

15,912 Comments

@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.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel