Digest Access Authentication

Digest Access Authentication Without mod_auth_digest

Digest Access Authentication is a better alternative and in my personal opinion a more secure authentication option then Basic HTTP Authentication. Although Basic Authentication is very easy to setup from our shared hosting cPanel, it has its week spots and the one that we are interested in particular, is that passwords are sent via the network in plain text format, making it vulnerable to different types of attacks .

This is the main reason why  Digest Access Authentication is preferred by many “geeks” and people who care about security. Unfortunately mod_auth_digest the Apache module needed for Digest Authentication to work is not always installed on the server, and since most users are on share hosting accounts they can not load the mod_auth_digest module themselves.

In this article we are going to get the Digest Access Authentication running with some PHP code and a simple .htaccess trick making it possible to authenticate users without having to worry if mod_auth_digest is installed on the server. For this example we are going to modify the Digest Access Authentication PHP code found on the PHP online manual.

Here is the original code:

<?php
    $realm = 'Restricted area';

    //user => password
    $users = array('admin' => 'mypass', 'guest' => 'guest');

    if (empty($_SERVER['PHP_AUTH_DIGEST'])) {
        header('HTTP/1.1 401 Unauthorized');
        header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"');
        die('Text to send if user hits Cancel button');
    }

    // Analyze the PHP_AUTH_DIGEST variable
    if (!($data = http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) || !isset($users[$data['username']])){
        die('Wrong Credentials!');
    }

    // Generate the valid response
    $A1 = md5($data['username'] . ':' . $realm . ':' . $users[$data['username']]);
    $A2 = md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']);
    $valid_response = md5($A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2);

    if ($data['response'] != $valid_response)
        die('Wrong Credentials!');

    // OK, valid username & password
    echo 'Your are logged in as: ' . $data['username'];

    // Function to parse the http auth header
    function http_digest_parse($txt){
        // Protect against missing data
        $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1);
        $data = array();
        $keys = implode('|', array_keys($needed_parts));

        preg_match_all('@(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $txt, $matches, PREG_SET_ORDER);

        foreach ($matches as $m) {
            $data[$m[1]] = $m[3] ? $m[3] : $m[4];
            unset($needed_parts[$m[1]]);
        }

        return $needed_parts ? false : $data;
    }
?>

Now, if you have mod_auth_digest installed and enabled on your server you will have no problems authenticating users with code. Buth if mod_auth_digest is not installed and enabled on your server, $_SERVER[‘PHP_AUTH_DIGEST’] will always be empty, thus sending the 401 header and popping out the username/password box no matter what you do.

We tried to make  this work without mod_auth_digest, but  we failed since no matter what we did the $_SERVER[‘PHP_AUTH_DIGEST’] variable was empty. This was the moment where we stopped looking for a “PHP Only” solution to our problem and started thinking out of the box (PHP). The solution came out to be a very simple one.

RewriteEngine on
RewriteRule .* - [E=DEVMD_AUTHORIZATION:%{HTTP:Authorization}]

Adding these two lines to a .htaccess file in the folder where the PHP script was located helped us solve the problem. But there is more. In order to use the new server environment variable to help us with the empty PHP_AUTH_DIGEST variable we will add a few lines of code that will check if the PHP_AUTH_DIGEST enviorment variable is empty and if it is empty set it to the new DEVMD_AUTHORIZATION variable we introduced with the .htaccess trick. So here is the complete code:

<?php
    $realm = 'Restricted area';

    //user => password
    $users = array('admin' => 'mypass', 'guest' => 'guest');

    // Here is the FIX
    if(empty($_SERVER['PHP_AUTH_DIGEST'])){
        $_SERVER['PHP_AUTH_DIGEST'] = $_SERVER['DEVMD_AUTHORIZATION'];
    }

    if (empty($_SERVER['PHP_AUTH_DIGEST'])) {
        header('HTTP/1.1 401 Unauthorized');
        header('WWW-Authenticate: Digest realm="'.$realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"');
        die('Text to send if user hits Cancel button');
    }

    // Analyze the PHP_AUTH_DIGEST variable
    if (!($data = http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) || !isset($users[$data['username']])){
        die('Wrong Credentials!');
    }

    // Generate the valid response
    $A1 = md5($data['username'] . ':' . $realm . ':' . $users[$data['username']]);
    $A2 = md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']);
    $valid_response = md5($A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2);

    if ($data['response'] != $valid_response)
        die('Wrong Credentials!');

    // OK, valid username & password
    echo 'Your are logged in as: ' . $data['username'];

    // Function to parse the http auth header
    function http_digest_parse($txt){
        // Protect against missing data
        $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1);
        $data = array();
        $keys = implode('|', array_keys($needed_parts));

        preg_match_all('@(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $txt, $matches, PREG_SET_ORDER);

        foreach ($matches as $m) {
            $data[$m[1]] = $m[3] ? $m[3] : $m[4];
            unset($needed_parts[$m[1]]);
        }

        return $needed_parts ? false : $data;
    }
?>

This article is just demo showing you how to get Digest Access Authentication running in PHP without the mod_auth_digest Apache module installed and enabled. It is published WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.