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:
2:
3: Logout 4: 6: 7:
8:
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