Introduction
Before we jump into our tutorial we should note a few points. Challenge/Response adds security to your login process by allowing a user to avoid sending their password as plain text. It's a relatively simple security step that is often neglected in PHP by both old and new programmers. It is not intended as the sole member of your security layer, and indeed should be complemented by all other more frequently preached practices. At its simplest it replaces the outgoing password with a unique impossible to re-use random string of characters which a server may accept instead of a password. It's primary benefit is preventing passwords being read from network traffic records being collected by would-be attackers.
What is a Challenge/Response Login Process?
First a few explanations!
Challenge/Response is a method of allowing a user to send their credentials (password/username) to a remote server, without sending their password as plain text. It adds security by preventing bad people (hackers, scriptkiddies, siblings and their ilk) from stealing your password as a result of listening/sniffing your network traffic.
It operates by allowing the client (the browser) to generate a unique once-off hash that can be matched by the server which can generate an identical hash in the EXACT same way. This is possible because the server will insert into all login forms a hidden field containing a unique "Challenge" for use by the client. The Challenge (yet another hash) will be stored for a limited time by the server (usually on a database) to allow it to regenerate the "Response" hash it expects from the client browser.
The "hash" (the result of the MD5, SHA1 or SHA256 algorithms) generated by the client browser is what we refer to as the "Response". In essence we are replacing the password being submitted with a constantly shifting, unpredictable and impossible to re-use string.
Basically we want to change what the client will send to the server from:
username=>myname
userpass=>mypass
to:
username=>myname
response=>generatedResponse
i.e. the client never sends the password in the clear to be easily read and stolen by the malevolent few who do such things
The Password Replacement: The Response
The Response as we call it, is the hash generated by the client based on our unique random time-limited Challenge. The actual makeup of a Response can vary between implementations. The makeup we use (before its hashed) is a string of form:
hashedpassword:lowercaseusername:challenge
"hashedpassword" is an MD5/SHA1/SHA256 hash of the user's password. We use a hashed password because the server will not actually know the plain text string used by the user - i.e. we are storing all passwords on the database in a hashed format (a common practice). By storing the hash of a plain password on our database we decrease the risk of the user's password being stolen, read and misused. Remember that the calculated hash of any string will always be the same - so knowing the actual password is not required and doing so is an unnecessary security risk.
"lowercaseusername"; acknowledging that the user is prone to errors, for example: not remembering the capitalisation of their username - we will put all usernames into lowercase to bypass such errors and typos. This lowercasing is unnecessary where usernames stored on the database are actually sensitive to capitalisation.
"challenge"; our server generated hash which is both random and time-limited, i.e. it's only ever used once and is invalidated after a predetermined period of time. It is regenerated on every request to the server to display the login form.
The essential idea to get clear is that the Challenge will change on every login. Since it is never the same Challenge, the same Response can never be sent from the client. Result: anyone reading our html requests will never be able to predict our Response. Equally they will never be able to re-use any previous Response and so will never be able to emulate our login, access the user's account, and do *bad things*.
I will qualify that statement by adding that nothing is impossible - there are other methods of gaining access to accounts (e.g. cookie stealing though XSS exploits). Security is a large area - but do research it! We only concentrate here on keeping passwords out of other people's hands as they are passed between client and server. It is very important that Challenge/Response is complemented by other security measures.
Required Database Tables
Before we get into technical details, we need a few essentials; database tables and a login form!
Our first table is a very limited user accounts table. The structure (which you can input to MySQL) is:
Code: Select all
CREATE TABLE `user_accounts` (
`userid` int(11) unsigned NOT NULL auto_increment,
`username` varchar(64) NOT NULL default '',
`password` varchar(64) NOT NULL default '',
UNIQUE KEY `userid` (`userid`)
) TYPE=MyISAM;username: 'devnetwork', and
password: '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8'
The password is actually a SHA256 hash of the string "password" (generated by feyd's SHA256 class). Many thanks to feyd for its implementation! I have added a simple hashpassword.php file to aid hashing for PHP newcomers to the download mentioned below. Usage: http://mydomain/hashpassword.php?password=xxxxx [echo SHA256 hash to screen]
Next up, we need a database table to store our Challenges. Challenges are time limited so we include a timestamp field. In order to identify a Challenge with a user we store the user's session id for future reference:
Code: Select all
CREATE TABLE `challenge_record` (
`challenge` varchar(64) NOT NULL default '',
`sess_id` varchar(64) NOT NULL default '',
`timestamp` int(11) NOT NULL default '0'
) TYPE=MyISAM;Challenge Generation and the Login Form
Our login form is saved as index.php - it has a few PHP steps at the start to generate and save a Challenge, and insert it into a hidden form field. This is how the server will communicate its generated Challenge, and its use explains why the client's Response is never used more than once.
Code: Select all
<?php
/*
Start the PHP Session
*/
session_start();
/*
Connect to database, and some table (say mytestdatabase)
Edit for your own credentials...
*/
$conn = mysql_connect('localhost', 'username', 'userpass') or die('Could not connect to database');
mysql_select_db('mytestdatabase', $conn) or die ('Can\'t use mytestdatabase : ' . mysql_error());
/*
We will use feyd's SHA256 PHP implementation to support SHA256 (does not require mcrypt enabled).
Depending on where you get your version ensure no echo() statements are left uncommented out
*/
require_once('sha256.inc.php');
/*
Generate a Challenge hash using feyd's class
*/
$challenge = SHA256::hash(uniqid(mt_rand(), true));
/*
All new Challenges are given a 6 minute lifetime. Delete all challenges with timestamps less than
current time() value. These have timed out, and must not be used even if the user has not yet
submitted their form.
*/
mysql_query("delete from challenge_record where sess_id = '" . session_id() . "' or timestamp < " . time()) or die("Invalid query: " . mysql_error());
/*
Store our generated Challenge to the database - and give 6 minutes of life before being invalidated
and deleted upon the next request to our login form.
*/
mysql_query("insert into challenge_record (sess_id, challenge, timestamp) values ('". session_id() ."', '". $challenge ."', ". (time() + 360) .")") or die("Invalid query: " . mysql_error());
/*
HTML for login form now follows
*/
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title>Catchy Title</title>
<meta http-equiv="content-type" content="text/html; charset=ISO-8859-1" />
<!--
Include a javascript implementation of the SHA256 algorithim
Download from: http://www.mad-teaparty.com/Chrstph/sha256.html
-->
<script language="javascript" src="sha256.js" type="text/javascript"></script>
<!--
Include a javascript function to manipulate our form data, i.e. to generate a Response string, delete
userpass and challenge prior to allowing submission. Rem: we don't want to send a plain text password!
-->
<script language="javascript" type="text/javascript">
<!--
function doChallengeResponse() {
str = document.login_form.username.value.toLowerCase() + ":" +
sha256_digest(document.login_form.userpass.value) + ":" +
document.login_form.challenge.value;
document.login_form.userpass.value = "";
document.login_form.challenge.value = "";
document.login_form.response.value = sha256_digest(str);
return false;
}
// -->
</script>
</head>
<body>
<h3>Challenge Response Login Form</h3>
<div><br />
<br />
<!--
Our form has 4 fields - but only 2 are submitted. The doChallengeResponse() javascript function
will generate a Response and set it as the value of 'response'. The same function will also unset
the value of the 'userpass' field, and 'challenge' field which we DO NOT want sent!
The javacript function is called when the user submits the form - see the onsubmit tag...
-->
<form method="post" action="login.php" name="login_form" id="login_form" onsubmit="doChallengeResponse()">
Username: <input type="text" name="username" id="username" value="" size="16" />
<br />
Password: <input type="password" name="userpass" id="userpass" value="" size="16" />
<br /><br />
<input type="reset" name="u_reset" id="u_reset" value="Reset" /> <input type="submit" name="u_submit" id="u_submit" value="Login" />
<!--
Insert the Challenge value from the server with a small PHP echo()
-->
<input type="hidden" name="challenge" id="challenge" value="<?php echo($challenge); ?>" />
<!--
Our 'response' field will be filled by the javascript function once the Response string is generated
-->
<input type="hidden" name="response" id="response" value="" />
</form></div>
</body>
</html>Explaining the Javascript Component
So we have our login form and Challenge generate/store PHP code in index.php. The comments should explain most of it. Our javascript function bears a little extra attention:
Code: Select all
function doChallengeResponse() {
str = document.login_form.username.value.toLowerCase() + ":" +
sha256_digest(document.login_form.userpass.value) + ":" +
document.login_form.challenge.value;
document.login_form.userpass.value = "";
document.login_form.challenge.value = "";
document.login_form.response.value = sha256_digest(str);
return false;
}We also hash our plain text password; remembering that our database will store an identical SHA256 hash and not the plain text version. Last we add our server generated Challenge (which is unique to all requests). The colon ':' separator is optional - I just like using it for reasons unknown and not to be discussed here
Next our javascript function deletes the 'userpass' and 'challenge' form fields which we don't want sent to the server; those bad people may be listening to our http requests! Finally we set the value of the 'response' field to a SHA256 hash of our constructed string - this is our Response hash.
Returning the boolean 'false' exits the function, returns control to our form, and allows the form submission to continue.
Something I will note at this stage. Some are probably rubbing hands with barely concealed glee at the prospect of sinking the grand ship Challenge/Response. Although this process requires javascript - javascript being disabled will NOT prevent a login. In the absence of javascript, a normal submit will take place - sending the username, and plain text password. Unfortunately users with javascript disabled will not have the added security of a Challenge/Response login process - but they will still be able to login normally. All we need do is add an extra step to check for the existence of a plain text password if our Challenge/Response process fails.
Point them cannons the otha' way, me maties! Har, har!
Authenticating the User with a Response Hash
Now, we have gotten as far as submitting our Response (or plain text insecure password for those few with javascript disabled in their browsers). Next we need to add a login.php file to test this data, and see whether our user will be authenticated. You can of course OOP this entire process at your leisure.
The file is included in the download pack at http://quantumstar.sourceforge.net/chal ... secode.zip. It is as follows:
Code: Select all
<?php
/*
Start the PHP Session
*/
session_start();
/*
Connect to database, and some table (say mytestdatabase)
Edit for your own credentials...
*/
$conn = mysql_connect('localhost', 'username', 'userpass') or die('Could not connect to database');
mysql_select_db('mytestdatabase', $conn) or die ('Can\'t use mytestdatabase : ' . mysql_error());
/*
Get our server stored Challenge from the database
Rem: ensure we only select Challenges which have not timed out!
Once we have a valid challenge from the database, we delete its record to prevent possible re-use.
This of course means a user requires a new challenge for every form submission.
*/
$result = mysql_query("select challenge from challenge_record where sess_id = '" . session_id() . "' and timestamp > " . time()) or die("Invalid query: " . mysql_error());
/*
Check we got a matching result
If this is not so, its most likely the Challenge has timed out - user waited too long to submit form
This is a small useability concern. You may of course expand the time limit. But don't expand it too far!
*/
if(mysql_num_rows($result) == 0)
{
header('Location: timedout.php'); //simple file with a die() statement - see the download pack
}
/*
Fetch the array containing the Challenge
*/
$c_array = mysql_fetch_assoc($result);
/*
After fetching the challenge we should take care to immediately delete it from the database.
This ensures the challenge is not available for re-use by any potential hacker who has
been listening to our network traffic. Challenges without this would remain valid for a 5 minute window.
*/
mysql_query("delete from challenge_record where sess_id = '" . session_id() . "'") or die("Invalid query: " . mysql_error());
/*
Filtering all incoming user data is essential. I'm not going to do so in-depth but bear this in mind for any
real life - live implementation!
*/
/*
We expect the username and Response to be alphabetic and numeral characters only (for this example at least)
For users without javascript we will assume their password should be alphanumeric also
Do not take the following validation of input as gospel - 'tis basic only maties...
There are two checks here. One if the client request contained a Response. The other if client sent a
password value (i.e. javascript was disabled - or bypassed if this is a constructed response from one
of those *bad people*)
*/
if(isset($_POST['response']) && !empty($_POST['response']) && (!ctype_alnum($_POST['username']) || !ctype_alnum($_POST['response'])))
{
// we may log bad data, or make the user walk the plank for their trouble!
die('Bad Input: Response or username are not alphanumeric!');
}
if(isset($_POST['password']) && !empty($_POST['password']) && (!ctype_alnum($_POST['username']) || !ctype_alnum($_POST['password'])))
{
// log or keel-haul the swabbies!
die('Bad Input: Password or username are not alphanumeric!');
}
/*
Execute a query to select User data based on the submitted username
Normally we would use some escaping here - its omitted for clarity (is magic_quotes dependent)
mysql_real_escape_string() is a good place to start...
*/
$result = mysql_query("select userid, username, password from user_accounts where username = '" . $_POST['username'] . "'") or die("Invalid query: " . mysql_error());
/*
Ensure we got a result
No result would indicate the User does not exist and must register an account
(code for registering is not included in this tutorial)
*/
if(mysql_num_rows($result) == 0)
{
header('Location: usernotexist.php'); // could just as easily be a signup form...
}
/*
Fetch the User data into an associative array
*/
$user = mysql_fetch_assoc($result);
/*
We're back to worship at the Altar of Feyd
Include feyd's PHP SHA256 implementation
*/
require_once('sha256.inc.php');
/*
Our database already stores a SHA256 hashed copy of the user's password
Storing plain text passwords on the database is bad - it may earn you a plank walk
Generate what we expect to be the client's Response using the same Challenge we initially sent them
Remember the Response string's construction:
- lowerstring username
- hashed password
- the unique time-limited once-off Challenge hash
*/
$response_string = strtolower($user['username']) . ':' . $user['password'] . ':' . $c_array['challenge'];
$expected_response = SHA256::hash($response_string);
/*
Compare the actual client Response hash against our expected Response hash
1. If they match, we will authenticate the user
2. If they don't, we will check if a plain text password exists (might be a client with javascript disabled), hash it, and compare to the database stored password hash
3. All other cases - we fail the authentication test, and boot the user (maybe direct to "Try Again" page)
*/
if($_POST['response'] == $expected_response)
{
$_SESSION['authenticated'] = 1;
$_SESSION['userid'] = $user['userid'];
header('Location: hello.php');
}
elseif(isset($_POST['userpass']) && !empty($_POST['userpass']))
{
/*
Response from client did not match expected Response
See if a plain text password exists (sent if the client has javascript disabled)
*/
if(SHA256::hash($_POST['userpass']) == $user['password'])
{
/*
Submitted plain text password from non-js client, when hashed, agrees to database stored password hash
We authenticate the User
*/
$_SESSION['authenticated'] = 1;
$_SESSION['userid'] = $user['userid'];
header('Location: hello.php');
}
else
{
/*
At this point:
- the non-js client's plain text password - when hashed - does not match the database stored password hash
This login attempt has failed - we should direct user to try again.
*/
$_SESSION['authenticated'] = 0;
header('Location: badlogin.php?err=pass');
}
}
else
{
/*
At this point:
- The client Response does not agree with the server generated Expected Response
This login attempt has failed - we should direct user to try again.
*/
$_SESSION['authenticated'] = 0;
header('Location: badlogin.php?err=response');
}
//EOF
?>All files referred to may be downloaded in one single zip archive at: http://quantumstar.sourceforge.net/chal ... secode.zip
Conclusion and Notes
Feel free to ask any questions ye may have. All comments, suggestions, stupid errors I made, etc. are welcome. Any further explanations needed I'll add.
For those wishing to go much further in building even more security (yes, there is definitely more...) into this, run a few forum searches for session security, input filtering (or data validation), escaping, XSS, and above all else - ask questions. 'Tis a fair bunch live on these forums - they will drop you a few hints.
Challenge/Response is just one method of adding security to authentication. But its a very nice, relatively painless process. It is also one of the few which are aimed at covering the transaction from the client-side of a login request. This tutorial exists to dispel any myths I have encountered about such a system being complex. Yes, the code looks long - but I think my comments demonstrate its relative simplicity to implement (as well as explain the file length
Note: This is a tutorial to specifically implement a Challenge/Response login process. An actual authentication process would implement additional security measures. These were omitted purely to aid clarity - not to suggest in any way they were unnecessary. Feel free to suggest additional improvements or post references to such below. The more the better.
All code above with the EXCEPTIONS of feyd's SHA256 class (sha256.inc.php which is licensed under the LGPL) and the SHA256 javascript implementation (sha256.js) - both of which are copyrighted by their respective authors - is released to public domain...for what its worth. Steal at will