Ok, so we hashed our password in our last entry, now what do we do with it? We can't "unhash" it. So how can we tell if the user gives us the correct password when they try to log in.
Well, to demonstrate this we are going to need a simple login form and a simple user service. I am using the same
userService.cfc as I have in previous examples. It can be instantiated in your onApplicationStart() like so
<cfset application.userService = CreateObject('component', 'cfc.userService').init("myDSN") />
We will get to what goes inside of userService.cfc shortly. First we need a simple login form.
<!--- If there is an error message, display it --->
<cfif IsDefined("errorMessage") AND Len(errorMessage)>
<!--- Login form --->
<cfform name="loginForm" action="#cgi.script_name#" method="post">
Username : <cfinput type="text" name="username" value="" /><br />
Password : <cfinput type="password" name="password" value="" /><br />
<cfinput type="submit" name="btnSubmit" value="Login" />
Next, above our simple login form, we need some code for handling our posted form.
<!--- If the form is not empty, it has been posted --->
<cfif IsDefined("form") and NOT StructIsEmpty(form)>
<cfif IsDefined("form.username") AND IsDefined("form.password")>
<!--- If all form fields are present, send to validation function --->
<cfset userValid = application.userService.validateUser(form.username, form.password)>
<!--- If form fields were missing, set user as invalid --->
<cfset userValid = false />
<!--- Check to see if a valid user was returned --->
<!--- if user was valid, reroute them to their myAccount page --->
<cfset session.isLoggedIn = true />
<!--- If not valid, set error message --->
<cfset errorMessage = "Invalid Username or Password" />
So, as you can see, Our code is checking to see if the form was posted, if it was posted, it checks to see if the
appropriate fields were included. This is important because hackers will try to intercept your forms and add and remove
fields to see if they can get around your login.
If everything checks out, validateUser() is invoked from the userService and the username and password are passed in.
If the function returns false, or if any of the form fields are missing, userValid is set to false and an error message is set. If the function returns true, then the user is re-routed to the myAccount.cfm page after a session variable has been set for persistence.
So now, what is going on inside of userService.validateUser()?
<cffunction name="validateUser" access="public" output="false" returntype="boolean">
<!--- Accept username and password as args --->
<cfargument name="username" required="false" default="" type="string" />
<cfargument name="password" required="false" default="" type="string" />
<!--- Set local var for query --->
<cfset var qGetUserInfo = "" />
<cfset var inputHash = "" />
<!--- Using the username, get the hashed password and the salt (If the username is invalid, then nothing will be returned) --->
<cfquery name="qGetUserInfo" datasource="#variables.dsn#">
SELECT u.password, us.salt
FROM users u
JOIN user_salts us ON u.userid = us.userid
where u.username = <cfqueryparam value="#arguments.username#" cfsqltype="cf_sql_varchar" />
<!--- if the query returns a record then the username was legit, lets look at the passwords --->
<!--- Set the input hash by concatenating the password that was passed in to the salt from the DB and hashing it witht he same hash function as when it was stored. --->
<cfset inputHash = Hash(arguments.password & qGetUserInfo.salt, 'SHA-512') />
<!--- Compare the inputHash with the hash we pulled from the db if they match, then the correct password was passed in --->
<cfreturn NOT Compare(inputHash, qGetUserInfo.password) />
<!--- If no user info was returned then it is a bad username, return false --->
<cfreturn false />
ValidateUser() is expecting two arguments: username and password. They are not required and will default to blank strings since they will fail our tests if they are blank (unless you have a user with a blank username and a blank password, which would be bad database design as neither should be allowed to be NULL).
Our local variables are set. We need one for the query and one for the inputHash. If you don't know why we are doing this, look at Adrian's post about the var scope.
Next we query the database for the previously hashed password and salt using the username in the where clause. If it returns a value, then the username was legit.
If the query returns a value, then we append the salt we pulled from the database onto our user input password. Finally, using the same hash function we used when we
created the original hash, we create inputHash.
We then compare the new hash(inputHash) to the original hash(qGetUserInfo.password) that we pulled from the database. If they match, then the correct password was submitted and TRUE is returned to our calling page and the user is re-routed to the myAccount.cfm page.
This is obviously a very simple example of how you would implement a login. I have not done much here to persist the users validation. We will save that for our discussion of session management.
DISCLAIMER: The code in this post has not been tested. I threw it together late tonight and did not have a DB server set up to test my queries, so please take it for what it is, which is a rough and simple example. I may try to get an entire working example available for download if people express interest in the comments.