AREST - RESTful Authentication for PHP 4/5
Version: 1.1
Author: Travis Estill (travisce.com)
Copyright (c) 2010 Travis Estill


Introduction:
-------------

AREST, short for RESTful Authentication, is a secure login script for 
PHP 4/5. The script adheres to the HTTP specification and the 
principles of Representational State Transfer (REST). Cookies are not 
used, and session IDs are never injected into your URLs.

With the support of JavaScript and XMLHttpRequest, web browsers can 
authenticate users with HTTP Authentication through an HTML form. 
Because this process usually relies on Basic authentication, Digest 
parameters are sent through a Basic Authorization header to provide 
a secure login mechanism.

Web browsers that do not support XMLHttpRequest or don't resend 
credentials will bypass the HTML form and use Digest access directly.

For security, user logins are set to expire after a set amount of 
time. Cross-domain authentication is also supported, and users can 
logout.

AREST is licensed under the Lesser General Public License (LGPL).

Please contact me at travisce.com if you find bugs or have any 
suggestions for AREST!


Installation:
-------------

1. Create the required MySQL tables. See the section below.

2. Copy the "arest" directory into your web root.

3. Configure the "arest/src/cfg.php" file. The following parameters
need to be modified as appropriate:
- AREST_BASE_URI
- AREST_MYSQL_HOST
- AREST_MYSQL_DB
- AREST_MYSQL_USER
- AREST_MYSQL_PWD
- AREST_OPAQUE
- AREST_NONCE_SALT

4. Restrict web access to the "arest/src/" directory. This directory
contains sensitive configuration data and log files.

5. (optional) Permit PHP write access to the "arest/src/log/" 
directory.

6. (optional) The included javascript files are "uncompressed".
For a high volume site, consider compressing these files with the
Google Closure Compiler or the YUI Compressor.


Using the script:
-----------------

Include AREST at the beginning of your script, modifying the path to 
the "arest" directory as necessary:

1: require_once($_SERVER['DOCUMENT_ROOT'] . '/arest/arest.php');

You can adjust the Digest "domain" directive for cross-URI
authentication. By default, this parameter consists of two URIs: 
the current Request-URI and the same URI at the "www" subdomain.
Other configuration parameters can be customized before the
"arest_auth()" function is called. Example:

1: $arest_cfg['domain'] = array('/test', '/example');
2: $arest_cfg['title'] = 'Test Realm';
3: $arest_cfg['msg_loading'] = '<p class="info">';
4: $arest_cfg['msg_loading'] .= 'Loading, please wait&hellip;</p>';

Make the call to "arest_auth()" directly after any configuration 
settings. The first function parameter is the realm string used for 
authentication (users are associated with this realm):

1: arest_auth('Test Realm - example.com');

To logout users, send a POST request to the server with the
following data: "arest-disable-nonce=[nonce]", where [nonce] is the
nonce sent in the Authorization header. This PHP/HTML code will 
accomplish this:

1: <form action="<?= AREST_REQUEST_URI ?>" method="post">
2:   <fieldset>
3:     <legend>Logout</legend>
4:     <input name="arest-disable-nonce" type="hidden"
5:            value="<?= htmlspecialchars(AREST_DIGEST_NONCE) ?>">
6:     <input type="submit" value="Logout">
7:   </fieldset>
8: </form>

With an open output buffer, you can send the "Authentication-Info"
header to the client:

1: arest_auth_info(ob_get_contents());
2: ob_end_flush();

Authentication parameters are accessible through the following 
constants after calling "arest_auth()":
- AREST_DIGEST_USERNAME
- AREST_DIGEST_REALM
- AREST_DIGEST_NONCE
- AREST_DIGEST_URI
- AREST_DIGEST_RESPONSE
- AREST_DIGEST_ALGORITHM
- AREST_DIGEST_CNONCE
- AREST_DIGEST_OPAQUE
- AREST_DIGEST_QOP
- AREST_DIGEST_NC

Optional authentication can be achieved by having the 
"arest_auth()" function return the result and working with that 
data, instead of killing the script. The result will not be returned
for HTTP requests from the AREST javascript code. These special
requests must be handled by AREST for operability.

The following code sets "$result" to an array with 1) the status 
code to be sent in the response, 2) the response message, 3) the 
nonce to be sent, and 4) flags returned from the function:

1: $realm = 'Test Realm - example.com';
2: $result = arest_auth($realm, AREST_RETURN);


Creating users:
---------------

The following example demonstrates creating a new user (assuming an 
open connection to your MySQL server):

1: $query = "INSERT INTO arest_users(username) VALUES('Mufasa')";
2: mysql_query($query);
3: $user_id = mysql_insert_id();
4: $ha1 = md5("Mufasa:testrealm@somehost:Circle Of Life");
5: $query = "INSERT INTO arest_digests(digest_ha1, user_id, realm) ";
6: $query .= "VALUES('{$ha1}', {$user_id}, 'testrealm@somehost')";
7: mysql_query($query);


Notes:
------

1. PHP in CGI mode under Apache requires a special .htaccess file.
See the section below on PHP running in CGI mode.

2. For IIS servers, make sure you disable all IIS authentication 
options on the target website, including the default "Integrated 
Windows Authentication".

3. Modifying the realm string in either the "arest_auth()" function
argument or the database will break all password digests associated 
with that realm. It's very wise to carefully choose your realm
strings to avoid this dilemma. Think of it as good URL design.

4. Cross-URI or cross-domain authentication requires the same realm
string for each location.

5. AREST introduces a non-standard "Auth-basic" quality of 
protection (qop). Requests can be sent with a Basic Authorization 
header that closely mimics the more secure Digest Authorization 
header. See the section below on "Auth-basic" quality of protection.

6. "Auth-int" quality of protection is implemented, but most 
browsers do not support it and PHP does not lend itself well to it. 
Requests with multi-part data (e.g., file uploads) will not succeed 
unless PHP is specially configured. See "arest/src/cfg.php".

7. While a user's password is protected and replay attacks are 
thwarted with Digest authentication, all other data is transmitted
in the clear. This may not be a problem for most applications,
but for those sending sensitive data, such as credit card numbers,
TLS/SSL is absolutely necessary.

8. Internet Explorer 6 (and older) does not include the query in the 
Request-URI when building its Authorization header. This causes the 
URI matching logic to fail. IE6 also fails to send the required 
"opaque" directive. These issues are solved by enabling 
AREST_IE6_COMPAT; the unfortunate side-effect is reduced security.

9. Colons (':') are not allowed in the username for compatibility 
with Basic authentication.

10. Each time a new nonce is added to the database, old nonces are 
deleted every 1 in 1,000 times (statistically) by default. The 
maintenance SQL query can be very resource-intensive depending on 
table size and server load.

11. The "Authentication-Info" header can be sent to the client 
conveying information regarding successful authentication. This 
header allows the client to verify the identity of the responding 
server. Unfortunately, browser support for it is spotty.


Create the required MySQL tables:
---------------------------------

CREATE TABLE arest_digests (
  digest_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT,
  digest_ha1 VARCHAR(32) NOT NULL,
  user_id MEDIUMINT UNSIGNED NOT NULL,
  realm VARCHAR(255) NOT NULL,
  PRIMARY KEY (digest_id),
  INDEX (user_id)
) ENGINE=InnoDB;

CREATE TABLE arest_nonces (
  nonce VARCHAR(32) NOT NULL,
  user_id MEDIUMINT UNSIGNED NULL,
  expiration DATETIME NOT NULL,
  disabled BOOL DEFAULT 0,
  nc INT UNSIGNED NOT NULL DEFAULT 0,
  PRIMARY KEY (nonce)
) ENGINE=InnoDB;

CREATE TABLE arest_users (
  user_id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT,
  username VARCHAR(30) NOT NULL,
  PRIMARY KEY (user_id),
  UNIQUE (username)
) ENGINE=InnoDB;

ALTER TABLE arest_digests
  ADD FOREIGN KEY (user_id)
    REFERENCES arest_users (user_id)
    ON DELETE CASCADE
    ON UPDATE CASCADE;

ALTER TABLE arest_nonces
  ADD FOREIGN KEY (user_id)
    REFERENCES arest_users (user_id)
    ON DELETE CASCADE
    ON UPDATE CASCADE;


PHP in CGI mode:
----------------

If PHP is running in CGI mode, the following is required 
in the .htaccess file directly under your web root:

RewriteEngine on
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1]


"Auth-basic" quality of protection:
-----------------------------------

Requests can be sent with the non-standard, reduced-security 
Auth-basic quality of protection (qop). This can be sent through 
either the Basic or Digest Authorization header but is intended for
Basic access, specifically through javascript/XMLHttpRequest where 
the enhanced security of proper Digest access is not well supported. 
This header closely replicates the behavior and security of genuine 
Digest access. The header is created as follows:

Authorization: Basic base64(random-str ":" base64(digest-response))
ha1 = md5(username ":" realm ":" password)
response = md5(ha1 ":" nonce ":" cnonce)
digest-response = (username="[username]", realm="[realm]", 
  nonce="[nonce]", opaque="[opaque]", response="[response]", 
  qop="auth-basic", cnonce="[cnonce]")

The key difference is that the response digest is calculated
without H(A2), the MD5 digest of the request method and URI. This
introduces new possibilities for replay attacks. The nonce-count is
also not sent, further reducing security. However, this method
still protects against most replay attacks with the expirable
nonce, and the required cnonce protects against chosen-plaintext 
attacks. The user's credentials are also still encrypted. 
If desired, support for auth-basic can be disabled through
"arest/src/cfg.php".


Acknowledgments:
----------------

The MD5 and Base64 javascript routines are courtesy of webtoolkit.
Many thanks to Paul James of peej.co.uk for his inspiring articles
on REST and demonstrations of HTTP authentication through 
javascript. Thomas Pike of xiven.com is appreciated for his PHP 
class implementing Digest authentication. Thanks to Sergey Ilinsky
for his excellent cross-browser XMLHttpRequest script.

http://www.webtoolkit.info/
http://www.ilinsky.com/
http://www.peej.co.uk/articles/http-auth-with-html-forms.html
http://www.xiven.com/sourcecode/digestauthentication.php
