Now that we know what we're working with, we're going to need to sit down and understand how our users will be defined and how they will authenticate with the system.

We'll start out with a really simple table to define our users. We can always come back later and add additional information.

users
------------------
user_id integer
user_hash string
user_name string
password string
email string
[...]

The user_id column is the primary key of our table. It's set to auto-increment, so each new user will have a unique user_id. We also create a unique user_hash for each user. The plan is that in cookies and forms we won't want to use the user_id as it would be too easy to change the cookie's value to a different number to try and log in with someone else's credentials. This user hash is simply going to be a UUID stripped of dashes. Why strip the dashes, you may ask? Well, that's rather simple: I think they don't look good. That and the fact that the dashes don't add to the uniqueness of the UUID. We can remove them without fear of corrupting our data. As for the password, we'll store it as a hashed string encrypted using the SHA-512 algorithm. This will convert the user's password to a 128-character long string.

A UUID, or Universally Unique IDentifier, is a string that is intended to be unique. It uses a combination of the computer's MAC address, the timestamp and several other magic components to generate this string.

Let's start by giving our table a test user to work with. This script was written for MySQL but should also work in most other SQL languages.

view plain print about
1INSERT INTO users ( user_hash, user_name, password, email ) VALUES (
2 '50F6E9B5D6765C61F23A6BD60D1EF328',
3 'Admin',
4 '67E1B608EB15C93A327D13BDD7D61EE9B38BAC84E5F4E005B2278953EE9A7A1D28EF4299A32BEC704F0E6D9A098B744C3051403D30E518A127E5BE5EAB459D02', -- squash
5 'your.email@here.com'
6);

Preparing our login screen

To display a login screen, we'll have to prepare an event in ColdBox. To keep concerns separated, we'll create a separate handler for users. This handler will do stuff that users do: log-in, log-out, manage profile, user options, etc. But we're getting ahead of ourselves. For now, let's just create an empty handler and save it as /handlers/users.cfc:

view plain print about
1<cfcomponent name="users" extends="coldbox.system.eventhandler" output="false">
2    
3    <cffunction name="init" access="public" returntype="users" output="false">
4        <cfargument name="controller" type="any">
5        <cfset super.init(arguments.controller)>
6        <cfreturn this>
7    </cffunction>
8
9</cfcompoment>

Now that our handler's set up, we need to give it an event. When we don't request a specific event from a handler, it looks for the default one: index. That's a perfect spot to put our login form in, so let's create that event:

view plain print about
1<cffunction name="index" access="public" returntype="void" output="false">
2        <cfargument name="event" type="any">
3
4        <cfset var loc = { rc=event.getCollection() } />
5        
6        <cfset loc.rc.xehLoginForm = getSetting( "SESBaseURL" ) & "/users/processLogin" />
7        
8        <cfset event.setView( "users/login" ) />
9    </cffunction>

Remember: when ColdBox is looking for an event, it will look in the component for the requested handler, and then call the method with the name of the requested action. For example, when we call index.cfm?event=users.index, it will call the index method of the users component. The first thing we're doing in our action is creating a local variable with a reference to the request collection. The request collection is a copy of all URL and FORM variables. It also has access to several of ColdBox's inherent methods. What we do next is create an eXit Event Handler. The purpose of this variable is to tell the view where it will be submitted to. This allows forms to be reused and is a best-practice in most MVC-based frameworks.

"The basic idea of SES URLs is to create a URL that is passing variables (variable=value pairs) but does not look like it is. Why? Because search engines don't always pick up links to pages where variables are passed." (source)

ColdBox automatically converts SES URLs to it's native format (index.cfm?event=handler.action).

ColdBox has a handy method called getSetting that allows you to access it's internal settings structure. One of these values is SESBaseURL. Basically, it is the complete link to your index.cfm file. After that all you need to do is add the event in the form /handler/action, or, in our case, /users/processLogin. We then tell ColdBox to display our form through the setView method.

view plain print about
1<cfoutput>
2<form name="login" action="#rc.xehLoginForm#" method="post">
3    <fieldset>
4        
5        <cfif event.valueExists( "login_error" )>
6            <p class="form-errors">#rc.login_error#</p>
7        </cfif>
8        
9        <div class="field">
10            <label for="user_name">User name:</label>
11            <input type="text" id="user_name" name="user_name" size="20" maxlength="100" tabindex="1" />
12        </div>
13        
14        <div class="field">
15            <label for="password">Password:</label>
16            <input type="password" id="password" name="password" size="20" maxlength="100" tabindex="2" />
17        </div>
18        
19        <div class="field-nolabel">
20            <input type="checkbox" id="persist" name="persist" value="1" /> <label for="persist" tabindex="3">Stay logged in?</label>
21        </div>
22        
23        <div class="field-nolabel">
24            <button class="submit" tabindex="4">Submit</button>
25        </div>
26        
27    </fieldset>
28    
29</form>
30</cfoutput>

Just a basic HTML form! We know that ColdBox provides us with the an rc struct, which is a reference to the request collection. On line 5 we use another of ColdBox's methods: event.valueExists(). This functions returns true if the requested variable exists in the request collection and false if it doesn't.

Working with the form data

We setup our page to submit to the processLogin action of the users handler. Before I paste the code here's a rundown of what this event needs to do:

  • Ensure the user name and password provided is valid;
  • If invalid, return to login page and display the error;
  • If valid, construct a structure with the user's info;
  • Store the user information in the session scope;
  • If the user wants to stay logged in, store a cookie;
  • Return the user to the main page.

Here's what I have right now to go through this:

view plain print about
1<cffunction name="processLogin" access="public" returntype="void" output="false">
2        <cfargument name="event" type="any" />
3        
4        <cfset var loc = { rc=event.getCollection() } />
5        
6        <cfset loc.authService = getPlugin( "ioc" ).getBean( "authenticationService" ) />
7        <cfset loc.authBean = loc.authService.authenticate(
8                user_name = loc.rc.user_name,
9                password = loc.rc.password
10            ) /
>

11            
12        <cfif NOT loc.authService.isAuthenticated( loc.authBean )>
13            <cfset loc.rc.login_error = loc.authBean.getError() />
14            <cfset index( event ) />
15            <cfreturn />
16        </cfif>
17        
18        <cfset loc.oUser = getPlugin( "ioc" ).getBean( "userService" ).getUser( user_id = loc.authBean.getUser_id() ) />
19        
20        <cfset loc.sUser = {
21                user_id = loc.oUser.getUser_id(),
22                user_hash = loc.oUser.getUser_hash(),
23                user_name = loc.oUser.getUser_name(),
24                email = loc.oUser.getEmail()
25            } /
>

26            
27        <cfset getPlugin( "sessionstorage" ).setVar( "userInfo", loc.sUser ) />
28        
29        <cfif event.getValue( "persist", false )>
30            <cfcookie name="squash_user_hash" value="#loc.sUser.user_hash#" expires="10" />
31        </cfif>
32        
33        <cfset setNextRoute( "general" ) />
34        
35        <cfset event.noRender( false ) />
36    </cffunction>

I'll start by going through the entire event, and then I'll show the code behind some of these objects. So, we start by asking our authentication service to check if the user's information is valid. If the information is incorrect, we set a variable in our request collection and call the previous event. As you can see, all we need to do is call the method associated to that event to have it fire again. If everything's OK we can use the user service to retrieve the user's remaining information and store it in a structure.

ColdBox has a built-in plugin that encapsulates calls to the session variable. It basically takes care of locking the scope whenever you need to read from/write into it. By calling the plugin and using it's setVar method we can place our user's information in the session scope. Then, we use ColdBox's event.getValue() method to return the persist variable. However, since it's a checkbox if it wasn't checked it won't exist. The second parameter of the method is the value returned if it doesn't exist. Handy, right? So, if the checkbox was checked, store a cookie with the user's hash in it. Once we're done we tell ColdBox to send the user back to the default action of the general handler, which in our case will be our home page. The last call to event.noRender() tells ColdBox not to show a page for this event.

I had planned to go show how I setup the authentication model today, but then I looked back at all that's already there and I thought it's probably more than enough for today. We at least covered how to submit and process forms in ColdBox, and saved the user's information in the session scope. Tomorrow we'll see more code from the authentication system.