AREST - RESTful Authentication for PHP 4/5
Version: 1.2.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 
required, 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. Create "arest/src/.htaccess" on the server consisting of 
"Deny from all". This will restrict web access to your sensitive 
configuration data and log files.

5. Create users with the included "mkusr.php" script. For security, 
uncomment the "die" line after using the script (or delete the 
script).

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

7. (optional) The included javascript files are "uncompressed".
For efficiency, consider compressing these files with a tool such as
the Google Closure Compiler or the YUI Compressor.


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;


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

Include AREST at the beginning of your script:

1: require_once($_SERVER['DOCUMENT_ROOT'] . '/arest/arest.php');
2: $realm = 'Test Realm - example.com';
3: arest_auth($realm);

The first function parameter of "arest_auth()" is the realm string 
used for authentication (users are associated with this realm).

If you want to use a custom config file, it can be set with 
"$arest_cfg_path" directly before "require_once()" in the 
example above.

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. The following is a sample 
logout form:

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. This header conveys information regarding 
successful authentication and allows the client to verify the 
identity of the responding server (limited browser support):

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


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 important 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 500 times (statistically) by default. The 
maintenance SQL query can be very resource-intensive depending on 
table size and server load.


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:
-----------------------------------

AREST introduces the non-standard 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 more possibilities for replay attacks. The nonce-count is
also not sent, further reducing security.

However, the expirable nonce protects against replay attacks that 
involve logging the Authorization header for later unauthorized 
access. Further, the required cnonce protects against 
chosen-plaintext attacks. The user's credentials are also still 
encrypted with Auth-basic.

Auth-basic support 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
