Recipe 8.9. Using HTTP Basic or Digest Authentication


8.9.1. Problem

You want to use PHP to protect parts of your web site with passwords. Instead of storing the passwords in an external file and letting the web server handle the authentication, you want the password verification logic to be in a PHP program.

8.9.2. Solution

The $_SERVER['PHP_AUTH_USER'] and $_SERVER['PHP_AUTH_PW'] global variables contain the username and password supplied by the user, if any. To deny access to a page, send a WWW-Authenticate header identifying the authentication realm as part of a response with status code 401, as shown in Example 8-17.

Enforcing Basic authentication

<?php header('WWW-Authenticate: Basic realm="My Website"'); header('HTTP/1.0 401 Unauthorized'); echo "You need to enter a valid username and password."; exit(); ?>

8.9.3. Discussion

When a browser sees a 401 header, it pops up a dialog box for a username and password. Those authentication credentials (the username and password), if accepted by the server, are associated with the realm in the WWW-Authenticate header. Code that checks authentication credentials needs to be executed before any output is sent to the browser, since it might send headers. For example, you can use a function such as pc_validate( ), shown in Example 8-18.

pc_validate( )

<?php function pc_validate($user,$pass) {     /* replace with appropriate username and password checking,        such as checking a database */     $users = array('david' => 'fadj&32',                    'adam'  => '8HEj838');     if (isset($users[$user]) && ($users[$user] == $pass)) {         return true;     } else {         return false;     } } ?>

Example 8-19 shows how to use pc_validate( ).

Using a validation function

<?php if (! pc_validate($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) {     header('WWW-Authenticate: Basic realm="My Website"');     header('HTTP/1.0 401 Unauthorized');     echo "You need to enter a valid username and password.";     exit; } ?>

Replace the contents of the pc_validate( ) function with appropriate logic to determine if a user entered the correct password. You can also change the realm string from "My Website" and the message that gets printed if a user hits "cancel" in her browser's authentication box from "You need to enter a valid username and password."

PHP 5.1.0 and later support Digest authentication in addition to Basic authentication. With Basic authentication, usernames and passwords are sent in the clear on the network, just minimally obscured by Base64 encoding. With Digest authentication, however, the password itself is never sent from the browser to the server. Instead, only a hash of the password with some other values is sent. This reduces the possibility that the network traffic could be captured and replayed by an attacker. The increased security provided by Digest authentication means that the code to implement is more complicated than just a simple password comparison. Example 8-20 provides functions that compute digest authentication as specified in RFC 2617.

Using Digest authentication

<?php /* replace with appropriate username and password checking,     such as checking a database */ $users = array('david' => 'fadj&32',                'adam'  => '8HEj838'); $realm = 'My website'; $username = pc_validate_digest($realm, $users); // Execution never reaches this point if invalid auth data is provided print "Hello, " . htmlentities($username); function pc_validate_digest($realm, $users) {     // Fail if no digest has been provided by the client     if (! isset($_SERVER['PHP_AUTH_DIGEST'])) {         pc_send_digest($realm);     }     // Fail if digest can't be parsed     $username = pc_parse_digest($_SERVER['PHP_AUTH_DIGEST'], $realm, $users);     if ($username === false) {         pc_send_digest($realm);     }     // Valid username was specified in the digest     return $username; } function pc_send_digest($realm) {     header('HTTP/1.0 401 Unauthorized');     $nonce = md5(uniqid());     $opaque = md5($realm);     header("WWW-Authenticate: Digest realm=\"$realm\" qop=\"auth\" ".            "nonce=\"$nonce\" opaque=\"$opaque\"");     echo "You need to enter a valid username and password.";     exit; } function pc_parse_digest($digest, $realm, $users) {     // We need to find the following values in the digest header:     // username, uri, qop, cnonce, nc, and response     $digest_info = array();     foreach (array('username','uri','nonce','cnonce','response') as $part) {         // Delimiter can either be ' or " or nothing (for qop and nc)         if (preg_match('/'.$part.'=([\'"]?)(.*?)\1/', $digest, $match)) {             // The part was found, save it for calculation             $digest_info[$part] = $match[2];         } else {             // If the part is missing, the digest can't be validated;             return false;         }     }     // Make sure the right qop has been provided     if (preg_match('/qop=auth(,|$)/', $digest)) {         $digest_info['qop'] = 'auth';     } else {         return false;     }     // Make sure a valid nonce count has been provided     if (preg_match('/nc=([0-9a-f]{8})(,|$)/', $digest, $match)) {         $digest_info['nc'] = $match[1];     } else {         return false;     }     // Now that all the necessary values have been slurped out of the     // digest header, do the algorithmic computations necessary to     // make sure that the right information was provided.     //     // These calculations are described in sections 3.2.2, 3.2.2.1,     // and 3.2.2.2 of RFC 2617.     // Algorithm is MD5      $A1 = $digest_info['username'] . ':' . $realm . ':' . $users[$digest_info['username']];     // qop is 'auth'     $A2 = $_SERVER['REQUEST_METHOD'] . ':' . $digest_info['uri'];      $request_digest = md5(implode(':', array(md5($A1), $digest_info['nonce'],  $digest_info['nc'],     $digest_info['cnonce'], $digest_info['qop'], md5($A2))));     // Did what was sent match what we computed?     if ($request_digest != $digest_info['response']) {         return false;     }     // Everything's OK, return the username     return $digest_info['username']; } ?>

If you're not using PHP 5.1.0 or later but are using PHP as an Apache module, you can use Digest authentication with code such as the HTTPDigest class by Paul James, which is available at http://www.peej.co.uk/projects/phphttpdigest.html.

Neither HTTP Basic nor Digest authentication can be used if you're running PHP as a CGI program. If you can't run PHP as a server module, you can use cookie authentication, discussed in Recipe 8.10.

Another issue with HTTP authentication is that it provides no simple way for a user to log out, other than to exit his browser. The PHP online manual has a few suggestions for log out methods that work with varying degrees of success with different server and browser combinations at http://www.php.net/features.http-auth.

There is a straightforward way, however, to force a user to log out after a fixed time interval: include a time calculation in the realm string. Browsers use the same username and password combination every time they're asked for credentials in the same realm. By changing the realm name, the browser is forced to ask the user for new credentials. Example 8-21 uses Basic authentication and forces a log out every night at midnight.

Forcing logout with Basic authentication

<?php if (! pc_validate($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW'])) {     $realm = 'My Website for '.date('Y-m-d');     header('WWW-Authenticate: Basic realm="'.$realm.'"');     header('HTTP/1.0 401 Unauthorized');     echo "You need to enter a valid username and password.";     exit; } ?>

You can also have a user-specific timeout without changing the realm name by storing the time that a user logs in or accesses a protected page. The pc_validate2( ) function in Example 9-10 stores login time in a database and forces a logout if it's been more than 15 minutes since the user last requested a protected page.

pc_validate2( )

<?php function pc_validate2($user,$pass) {     $safe_user = strtr(addslashes($user),array('_' => '\_', '%' => '\%'));     $r = mysql_query("SELECT password,last_access                       FROM users WHERE user LIKE '$safe_user'");     if (mysql_numrows($r) == 1) {         $ob = mysql_fetch_object($r);         if ($ob->password == $pass) {             $now = time();             if (($now - $ob->last_access) > (15 * 60)) {                 return false;             } else {                 // update the last access time                 mysql_query("UPDATE users SET last_access = NOW()                              WHERE user LIKE '$safe_user'");                return true;             }         }     } else {         return false;     } }

8.9.4. See Also

Recipe 8.10; the HTTP Authentication section of the PHP online manual at http://www.php.net/features.http-auth.




PHP Cookbook, 2nd Edition
PHP Cookbook: Solutions and Examples for PHP Programmers
ISBN: 0596101015
EAN: 2147483647
Year: 2006
Pages: 445

Similar book on Amazon

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net