ColdFusion in Context: Login Included

Suppose you need to have users log in to your application. A typical approach is to have the page that evaluates the login use the cflocation tag to send the browser to a new page if successful. Because the page never finishes loading, getting the cookie set takes intermediate pages or a refresh. Another approach is to use javascript to automatically submit a form on the authentication page without user intervention, but it won't work without javascript. Here's another way.

Data

For a convincing demonstration, you'll need data. Call the table LoginInc. Give it an auto-numbered field UserID. Add text fields Username, Password, Nickname, and Role. Create a record or two.

Overview

You'll have a home page (home.cfm in this example) that has an image link to a login page (mat/image.cfm). The login page will accept a username and password and provide a message if there's a problem. The form on the login page is submitted to the login page itself. When the login page sees that the form has been submitted, it queries the table. If it finds a match, it includes a page (mat/ok.cfm) that gives the user a pass both in a cookie and in variables available immediately to the current page and also includes a page (act/menu.cfm) that provides the main menu within the application. If it doesn't find a match, it complains and gets ready to accept another attempt.

There are nuances. You'll need to supply an image for the link to the login page. The home page removes an existing pass (if any) from the cookie to avoid confusion. The welcome mat directory (mat) has an Application.cfm that enables session management, sets a datasource, and provides a key to encrypt the pass for the cookie. If the user's session times out, or the user blunders into a forbidden area, a bouncer (mat/bounce.cfm) tells the user what happened and returns the user to the login. For this demo, meeting.cfm says hello to prove that your login has been successful.

Extend Welcome; Permit Login

Make home.cfm a splash page that has an image link to mat/image.cfm. Once you reach mat/image.cfm, all the other files can be reached by a consistent path that looks like this: ../{directory}/{filename}.

<!--- home.cfm --->
<a href="mat/image.cfm">
<img src="earth.gif" width="60" 
height="60" border="0"></a><br>
! SPLASH !

Browsing pages in the mat directory includes ../mat/Application.cfm. In ../mat/Application.cfm, name an application, turn session management on, but turn the associated cookies off so you can set your own temporary ones. Set the datasource; set a pass key.

<!--- mat/Application.cfm --->
<!--- Enable session management --->
<cfapplication name="LoginInc"
sessionmanagement="yes"
setclientcookies="no">

<!--- Set datasource and pass key --->
<cfset request.Db="context">
<cfset request.PassKey="useyourown">

Make image.cfm handle login. Why name it image.cfm? To keep the login a surprise. It won't fool anyone looking for it, but the name supports the initial air of wonder that people have when they reach your site.

First, set defaults by expiring cookies and defining username and password as empty.

<!--- mat/image.cfm --->
<!--- Set defaults --->
<cfcookie name="Pass" value="0" expires="-1">
<cfset Message="">
<cfif not isDefined("form.UserName")>
  <cfset Username="">
  <cfset Password="">
</cfif>

Next, act on a login request that's been submitted (if any). Query the table. If a row is found, include ../mat/ok.cfm, include ../act/menu.cfm, and use cfabort to stop processing this page. If not, complain.

<!--- Act on login --->
<cfif isDefined("form.Try")>
  <cfquery name="loginGet" datasource="context">
  select * from LoginInc
  where Username = '#form.Username#'
  and Password = '#form.Password#'
  </cfquery>
  <cfif loginGet.recordcount>
    <!--- Issue pass --->
    <cfinclude template="../mat/ok.cfm">
    <!--- Move to menu --->
    <cfinclude template="../act/menu.cfm">
    <cfabort>
  <cfelse>
    <!--- Complain --->
    <cfset Message="Incorrect Login">
  </cfif>
</cfif>

Finally, accept a login attempt. Later on, you could add a link to get back to the splash page. But, this will do for now.

<!--- Accept login --->
<cfoutput>#Message#</cfoutput>
<form name="login" action="../mat/image.cfm" method="post">
<strong>Please log in . . . </strong><br>
Username <input type="text" name="Username"
size="25" maxlength="25"
value=<cfoutput>"#Username#"</cfoutput>><br>
Password <input type="password" name="Password"
value=<cfoutput>"#Password#"</cfoutput>>  <input
type="submit" name="Try" value="Go">
</form>

Include ../mat/ok.cfm to issue credentials. Because mat/Application.cfm enabled session management, a session has already been assigned. So, read the current session (cfid and its partner, cftoken). Then, store the Userid and Role in memory for this session.

<!--- mat/ok.cfm --->
<!--- Get and modify session --->
<cflock scope="session" timeout="30" type="exclusive">
<cfset request.cfid=session.cfid>
<cfset request.cftoken=session.cftoken>
<cfset session.Userid=loginGet.Userid>
<cfset session.Role=loginGet.Role>
</cflock>

When this is done, store Userid, cfid, and cftoken in a urlEncoded, encrypted cookie along with some random padding to discourage the curious. (The urlEncoding ensures that the result is viewable during debugging.) Out of seven items in the string, only items 2, 4, and 6 are useful. The rest are padding that will vary within separate ranges. Because Randomize doesn't live up to its name in my environment - it cause randRange to return the same number over and over - I don't invoke randomize first. Simply provide the range in which the number should fall. When all the pieces of the list have been defined, encrypt the whole list with the pass key in mat/Application.cfm. You could encrypt it multiple times, but this will do for this demonstration.

<!--- Write padded, encrypted, encoded cookie --->
<cfscript>
P1=randRange(0,150);
P2=loginGet.Userid;
P3=randRange(200,350);
P4=request.cfid;
P5=randRange(400,550);
P6=request.cftoken;
P7=randRange(600,750);
PassRaw="#P1#,#P2#,#P3#,#P4#,#P5#,#P6#,#P7#";
</cfscript>
<cfset request.Pass=urlEncodedFormat(encrypt(PassRaw,request.PassKey))>
<cfcookie name="Pass" value="#request.Pass#">

Before leaving this page, consider that the first step after leaving this page will be to include act/menu. All pages browsed in the act directory will use the cookie and session memory to determine userid and role. However, the menu isn't browsed the first time; it's included and therefore won't see the cookie. The page is still loading. Therefore, you need to provide credentials it can read(request.Role and request.Userid) for the first time the page is used.

<!--- Provide first-page credentials --->
<cfset request.Role=loginGet.Role>
<cfset request.Userid=loginGet.Userid>

Provide a Menu and a Function

Menu.cfm isn't difficult to write. Block unapproved activity. If the user asks to exit, include the login page and use cfabort to cease processing this page. If the user makes a regular choice, include that page and use cfabort to cease processing this page. Otherwise, display the menu and accept a choice.

<!--- act/menu.cfm --->
<!--- Block if needed --->
<cfparam name="request.Role" default="">
<cfif request.Role does not contain "A">
<cfoutput>#Message#</cfoutput>
  <cfset Message="You cannot perform this function.">
  <cfinclude template="../mat/bounce.cfm">
  <cfabort>
</cfif>

<!--- Act on choices --->
<cfparam name="form.Pick" default="">
<cfif form.Pick is "Exit">
  <cfinclude template="../mat/image.cfm">
  <cfabort>
<cfelseif form.Pick contains "Meeting">
  <cfinclude template="../act/meeting.cfm">
  <cfabort>
</cfif>

<!--- Provide menu --->
<form name="menu" action="../act/menu.cfm" method="post">
<input type="submit" name="Pick" value="Meeting Schedule">
<input type="submit" name="Pick" value="Exit">
</form>

Browsing any page in the act directory includes act/Application.cfm, which identifies the user and looks up the user's privileges. This code must set defaults, read the cookie, enable session management based on the session in the cookie, get ColdFusion to read that session from memory, and confirm that the user identified by the cookie is the same user identified in memory for this session.

To begin, set the datasource, set a pass key, and set a good default status.

<!--- act/Application.cfm --->
<!--- Set defaults --->
<cfset request.Db="context">
<cfset request.PassKey="useyourown">
<cfset SessionOK=1>

Try to read the Pass: an encoded, encrypted, and padded cookie value. Decode it. Decrypt it with the pass key. If the result is not a list with seven items, declare the session bad. The second item should be the Userid, the fourth the cfid, and the sixth the cftoken. Tell ColdFusion that it has found the cfid and cftoken in the url. (These won't actually show up in the URL.) When you subsequently enable session management, it will find them there and try to "continue" the session they represent.

<!--- Try to read an encoded, encrypted, padded cookie --->
<cfparam name=cookie.Pass default="">
<cfif len(trim(cookie.Pass))>
  <cfset PassRaw=decrypt(urlDecode(cookie.Pass),request.PassKey)>
  <cfif listLen(PassRaw) is 7>
    <cfset request.UseridPass=listGetAt(PassRaw,2)>
    <cfset url.cfid=listGetAt(PassRaw,4)>
    <cfset url.cftoken=listGetAt(PassRaw,6)>
  <cfelse>
    <cfset SessionOK=0>
  </cfif>
<cfelse>
  <cfset SessionOK=0>
</cfif>

Enable session management, but don't let ColdFusion write its default cookies. (You want to use only the Pass that you wrote at login.) Because ColdFusion will see the url variables you have just "written", it will look in memory for the session they represent. Read session variables into request variables (so you only have to lock them once). If you can't read the session, declare the session bad.

<!--- Enable session management --->
<cfapplication name="LoginInc"
setclientcookies="no"
sessionmanagement="yes">

<!--- Try to read memory --->
<cflock scope="session" timeout="30" type="readonly">
<cfif isDefined("session.Userid")>
  <cfset request.cfid=session.cfid>
  <cfset request.cftoken=session.cftoken>
  <cfset request.Userid=session.Userid>
  <cfset request.Role=session.Role>
<cfelse>
  <cfset SessionOK=0>
</cfif>
</cflock>

Compare the Userid from the cookie with the Userid in session memory. If they don't match, declare the session bad.

<!--- Compare Userids from two sources --->
<cfif SessionOK>
  <cfparam name="request.Userid" default=0>
  <cfparam name="request.UseridPass" default=-1>
  <cfif request.UseridPass is not request.Userid>
    <cfset SessionOK=0>
  </cfif>
</cfif>

If the session is bad, bounce the user. To do this, create a message that tells the user what you're doing. Include ../mat/bounce.cfm (which will display that message). Use cfabort to end processing of this page.

<!--- Bounce the user if there's a problem --->
<cfif not SessionOK>
  <cfparam name="request.Userid" default=0>
  <cfparam name="request.cfid" default=0>
  <cfparam name="request.cftoken" default=0>
  <cfparam name="request.Role" default="">
  <cfset Message="Your session is not active; you will need to log in again.">
  <cfinclude template="../mat/bounce.cfm">
  <cfabort>
</cfif>

The code for mat/bounce.cfm is simple. It's just a form. However, having all unexpected exits call this page provides the opportunity for a consistent interface.

<!--- mat/bounce.cfm --->
<cfoutput>#Message#</cfoutput>
<form action="../mat/image.cfm" method="post">
<input type="submit" name="doit" value="OK">
</form>

As is usual with most journeys, the destination is less imposing than the trip. Here's act/meeting.cfm. If the role isn't appropriate, it bounces the user. (You may want to stop in place with cfabort and display "press your back button to continue" instead of kicking the user out of the application.) If the user wants to exit, the page sends the user to the menu. Otherwise, it confirms success and provides a button that lets you leave when you're done admiring your handiwork.

<!--- act/meeting.cfm --->
<!--- Block if needed --->
<cfparam name="request.Role" default="">
<cfif request.Role does not contain "A">
  <cfset Message="You cannot perform this function.">
  <cfinclude template="../mat/bounce.cfm">
  <cfabort>
</cfif>

<!--- Act on choices --->
<cfparam name="form.Exit" default="">
<cfif len(trim(form.Exit))>
  <cfinclude template="../act/menu.cfm">
  <cfabort>
</cfif>

<!--- Your function goes here --->
HELLO.  You made it.

<form action="meeting.cfm" method="post">
<input type="submit" name="Exit" value="goodbye">
</form>

Review the Pass

Browse home.cfm. [For the on-line demonstration, the username is "many" and the password is "one".] Satisfy yourself that the login page won't be fooled. Satisfy yourself that you can't use the menu or the function without being logged in.

To apply these ideas to your own work, a few pointers are in order. Notice that when the menu is first included that the cookie isn't set yet. So, feed the menu what it would have received from Application.cfm in the working directory. When the menu or other pages in the working directory are browsed, Application.cfm reads the session identification from the encrypted cookie, tells ColdFusion it was in the URL, causes ColdFusion to read that session from memory, and makes sure the Userid in the cookie matches the Userid in memory. Avoid using cflocation; cfinclude lets you work with cookies more reliably.

The subject of security comes up whenever login and access are mentioned. Unless you expect the user to log in again for each page, you need to pass some kind of token from page to page, and this is pretty secure. Unless the encryption is broken - you can use more creative encryption than the demo uses - the worst that might reasonably happen is that an individual physically copies a cookie from someone else's machine. It would have to happen while that someone else was still using it; because, the cookie is erased when the browser closes. A stolen cookie will be still be useless unless that someone else currently has an active session AND that session is the same session as identified by the cookie. Remember that sessions in memory time out. If the Userid and session in the cookie don't match an active session that's already using that Userid, no access is gained.

This application doesn't set permanent cookies; it doesn't post a visible URL. In the absence of plain session identification in URLs or cookies, ColdFusion provides a fresh session each time (until it runs out and recycles session numbers). Even if a user logs in several times in succession, the user will receive a different session each time. Note that someone trying to add session identification to the actual URL can't force a specific session to be used in an effort to use just part of the cookie credential; because, any session identification in the URL is overwritten by the session identification in the cookie, and inability to read the cookie blocks access. Therefore, an unscrupulous person can't count on cookie theft (which requires close timing and an unlikely degree of physical access in the first place) to get into your application.

Now you're without excuses to add login where appropriate. Login is included. =Marty=