Challenge/Response Secure Login Process

Tutorials on PHP, databases and other aspects of web development. Before posting a question, check in here to see whether there's a tutorial that covers your problem.

Moderator: General Moderators

Challenge/Response Secure Login Process

Postby Maugrim_The_Reaper » Thu Sep 29, 2005 9:01 am

Having referred to Challenge/Response in relation to user logins at least four (ed: five) times now...within the space of a week - I present the following tutorial on the topic.



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:

Syntax: [ Download ] [ Hide ]
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;


Into this we will require at least one user account. You can use the following data to add an account:

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:

Syntax: [ Download ] [ Hide ]
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.

Syntax: [ Download ] [ Hide ]
<?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:&nbsp;&nbsp;<input type="text" name="username" id="username" value="" size="16" />
        <br />
        Password:&nbsp;&nbsp;<input type="password" name="userpass" id="userpass" value="" size="16" />
        <br /><br />
        <input type="reset" name="u_reset" id="u_reset" value="Reset" />&nbsp;&nbsp;<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>


I know how tiring it can be to have to copy and paste - so all the files I refer to are downloadable from http://quantumstar.sourceforge.net/challengeresponsecode.zip ;) The text of this tutorial is likewise included. Files are currently saved in Windows file format - easier on those new to PHP using Windows.



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:

Syntax: [ Download ] [ Hide ]
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;
  }


As you can see, the first thing we do is construct the string that will form the basis of our hashed Response. The [document.login_form.username.value.toLowerCase()] statement ensures our username is in lowercase letters (this ignores any included numerals). This allows us to ignore errors in the capitalisation of the typed username. Recall that its use depends on whether your usernames are designed to be sensitive to capitalisation or not.

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/challengeresponsecode.zip. It is as follows:

Syntax: [ Download ] [ Hide ]
<?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



?>


Please read all comments in code to follow the process. Me wrists be a tirin' ;) One small note is that per HTTP/1.1 our "Location" headers should be supplied a full URI. A relative URI will in most cases work - but keep the recommended practice in mind.

All files referred to may be downloaded in one single zip archive at: http://quantumstar.sourceforge.net/challengeresponsecode.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 ;)). With the capacity to support non-javascript enabled clients - keep it in mind for your next login script.

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 ;)
Pádraic Brady

http://blog.astrumfutura.com
http://www.survivethedeepend.com
Zend Framework Community Review Team
Zend Framework PHP-FIG Representative
User avatar
Maugrim_The_Reaper
DevNet Master
 
Posts: 2704
Joined: Tue Nov 02, 2004 6:43 am
Location: Ireland

Re: Challenge/Response Secure Login Process

Postby Dave2000 » Thu Jul 15, 2010 12:04 pm

Thanks Maugrim - a very nice read. One thought though: surely this method means that salting cannot be used with passwords in the DB...?
Dave2000
Forum Contributor
 
Posts: 126
Joined: Wed Jun 21, 2006 1:48 pm

Re: Challenge/Response Secure Login Process

Postby pkphp » Mon Sep 20, 2010 1:31 am

So nice an article about security. Thank you very much for you share.
I must say , this is so helpful for us a new phper.
pkphp
Forum Newbie
 
Posts: 12
Joined: Mon Sep 20, 2010 1:20 am

Re: Challenge/Response Secure Login Process

Postby almedajohnson » Tue Sep 21, 2010 11:13 pm

That's really nice article. You have made it very easy to understand by using simple language and avoiding the jargons. IT will be surely helpful to everyone working on PHP security weather he is a beginner or expert.. Good on you.
almedajohnson
Forum Newbie
 
Posts: 7
Joined: Sun Sep 19, 2010 11:21 pm


Return to Tutorials

Who is online

Users browsing this forum: No registered users and 2 guests