Secure Logins with Challenge-Response

Jan 14th, 2007No Comments

Background

No matter how securely you code your site, someone with a valid password can access that which you mean to keep private. The best solution is, of course, to keep any and all pages that require the input of a password protected using an SSL certificate. In reality, not all sites can afford the luxury of their own SSL certificate. Another, entirely free, method of keeping passwords from being transmitted across the web in clear text is to use a challenge-response system for logins.

The basic idea behind a challenge-response system is that the page requesting a password will have some extra information embedded in it – information that is unique to that particular login attempt – which is used to encode the password before it is sent across the network. In this tutorial, we’ll see how to generate this extra information, called the salt and how to incorporate it into a secure hash using SHA1

The work upfront

First of all, we’ll take a look at things on the client end of things. We will need a javascript implementation of the SHA1 algorithm. I found one as done by a guy named Chris Vaness. I am not sure where I found it, but I did maintain his copyright as requested. Grab it for your own use here: sha1.js. For CakePHP, place this file in the app/webroot/js directory. All you really need to know about this file is that it defines a function sha1Hash that takes one argument – the string to hash – and returns the hashed value.

Now we need a form for logging in to a site. The html code for a simple login is shown in snippet #1.

Snippet #1: HTML code for our simplified login.

<p class="error_message">The login credentials you supplied could not be recognized. Please try again.</p>
 
<form method="post" action="/admin/login">
    <label class="label" for="username">Username:</label>
    <?php input('User/username', array('size' => 20, 'class' => 'TextField', 'id'=>'username')); ?>
    <?php tagErrorMsg('User/username', 'Username is required.')?>
    <label class="label" for="password">Password:</label>
    <?php password('User/password', array('size' => 20, 'class' => 'TextField', 'id'=>'password')); ?>
    <?php tagErrorMsg('User/password', 'Password is required.')?>
    <?php submit('Login', array('class'=>'Button'));?>
</form>

Of course, the code in Snippet #1 is just the login bits of the page, you’ll want to embed that in a full page (and hopefully pretty it up with some CSS) for production use. In our controller, we’ll need to generate the salt to embed in the page. Code for doing this is shown in Snippet #2.

Snippet #2: Adding the salt in the controller.

function login()
{
    if(!empty($this->data))
    {
         //handle the login - we'll get to this later
    }
    else
    {
        //create the salt by creating an MD5 Hash of the current time
        $salt = md5(time());
        //make the salt available to the view
        $this->set('special_sauce',$salt);
        //store the salt in the session
        $this->Session->write('salt', $salt);
    }
}

As you can see, generating the salt and making it available in a variable which we can access in the view is really a trivial matter. A couple of points bear explanation. First of all, why am I using MD5 here when using SHA1 for the hashing itself? Well, this particular use of a hash is simply to generate a random assortment of characters to use as salt, therefore we can use the less secure MD5 algorithm which will run a little faster than SHA1. It really makes no difference, and is more a matter of preference than anything else. For the actual hashes, however, you should use SHA1 as it is more secure. Secondly, you may wonder why I am saving the salt in the session when I have made it available to the view (yes, I am going to be placing it in a hidden form element). The reason for this is to ensure that we use the salt generated here when we do our comparisons later on. If we were to place it in the form element and use that return to check the stored password, an attacker could simply supply the salt he wanted to use in that form element and he could bypass our security. This way, we are protected against clever people like that.

With our form defined and our salt available we need to write some javascript to process the user input before sending it to the server. Specifically, we are going to take the entered password, run the SHA1 algorithm on it, concatenate the resulting hash with the salt and run the algorithm on it again. Following that process we write our new password value into the password field and let the form submit. Javascript to accomplish this is shown in Snippet #3.

Snippet #3: Javascript transform_login function

function transform_login()
{
  var password = document.getElementById('password').value;
  var salt = document.getElementById('special_sauce').value;
 
  var hashed_pass = sha1Hash(password);
  var hashed_and_salted_pass = hashed_pass + salt;
  var salted_hash_pass_hash = sha1Hash(hashed_and_salted_pass);
 
  document.getElementById('password').value = salted_hash_pass_hash;
 
}

This new javascript function requires that we make some changes to our login code, as shown in Snippet #4.

Snippet #4: Modfied login form.

<script src="/js/sha1.js" language="javascript"></script>
<script src="/js/login.js" language="javascript"></script>
<?php if ($error): ?>
    <p class="error_message">The login credentials you supplied could not be recognized. Please try again.</p>
<?php endif; ?>
<form action="/admin/login" method="post")>
        <label for="username" class="label">Username:</label>
        <?php echo $html->input('User/username', array('size' => 20, 'class' => 'TextField', 'id'=>'username')); ?>
        <?php echo $html->tagErrorMsg('User/username', 'Username is required.')?>
        <label for="password" class="label">Password:</label>
        <?php echo $html->password('User/password', array('size' => 20, 'class' => 'TextField', 'id'=>"password")); ?>
        <?php echo $html->tagErrorMsg('User/password', 'Password is required.')?>
        <?php echo $html->submit("Login", array('class'=>'Button', 'onclick'=>"Javascript:return transform_login();")); ?>
        <input type="hidden" name="special_sauce" id="special_sauce" value='<?php echo $special_sauce; ?>'>
</form>
<?php if ($error): ?>
    <script language="javascript">
        document.getElementById('password').value = "";
        document.getElementById('username').value = "";
    </script>
<?php endif; ?>

As you can see, we have added code to include the sha1.js and login.js files which contain our sha1 functions and login function transform_login. We have also inserted our hidden value with the salt in it, called special_sauce here. Finally, we have added an onclick to the submit button to call the javascript function and transform our password. This should be everything we need on the client end of things.

The login process in the controller

Earlier, in Snippet #2, we passed over the code in the controller that actually handles the processing of the login. Now it is time to revisit the controller and finish off our secure login. The goal here is to replicate the actions taken in the javascript function transform_login and compare the results of the server-side transform and that done on the client end.

Since we embedded the salt in the login form using a hidden form value, that value will be available to us here in the controller. We never want to use that value, however, because we can never be sure of the source of any information coming to us from a web form. It is very easy to construct a duplicate form which submits faulty information in an attempt to gain access to a secured system. What we will do is take the password from the user’s database record and the salt stored in the session and put that information through the same transformations as we did on the client side. If the values match then we know the user entered the right password and can be granted access. Snippet #5 contains the full code of the final controller method.

Snippet #5: Completed controller login code.

function login()
{
     // If a has submitted form data:
     if (!empty($this->data))
     {
         $someone = $this->User->findByUsername($this->params['data']['User']['username']);
 
         //the password from the database
         $password = $someone['User']['password'];
         //the salt value from the session
         $salt = $this->Session->read('salt');
         //hash the password
         $hashed_password = sha1($password);
         //add the salt to the hashed password
         $salted_and_hashed_password = $hashed_password . $salt;
         //salt the salted and hashed password
         $salted_hash_pass_hash = sha1($salted_and_hashed_password);
 
         //do our comparison
         if($salted_hash_pass_hash == $this->params['data']['User']['password'])
         {
              // This means they were the same. We can now build some basic
              // session information to remember this user as 'logged-in'.
              $this->Session->write('User', $someone['User']);
 
              // Now that we have them stored in a session, forward them on
              // to a landing page for the application.
              $this->redirect('/logged_in/');
         }
         // Else, they supplied incorrect data:
         else
         {
             //Remember the $error var in the view? Let's set that to true:
             $this->set('error', true);
         }
     }
     else
     {
         $salt = md5(time());
         $this->set('special_sauce',$salt);
         $this->Session->write('salt', $salt);
     }
}

Summary and Caveats

That’s really all there is to securing your logins with a challenge-response system. Of course, if you have an SSL certificate available to you, using it would be in your best interests. There is, however, nothing stopping you from using both at the same time. You can never have too much security.

A few words of warning, however. This is not intended to be a complete solution for your login needs. There are several aspects of a login system that have been overlooked in the name of brevity. Not the least of these are validating required fields and protecting all input fields from SQL Injection vulnerablilities. This only provides you with the means of obscuring password data in transit over public networks. I recommend that you do your research up front on a complete solution to prevent getting burned by something simple you may have overlooked.

About author:

A software developer currently working primarily with Python on Google App Engine.

All entries by

Leave a Reply