This module is a form-based authorization module
based on '
mod_auth_mysql'
and
'
mod_auth_sim'.
It is used to place access restrictions on a
per-directory, per-user-request basis using session management.
The module uses a MySQL database to retrieve users' group
membership, maintain and validate users' sessions, and
optionally track user activity.
The mechanics of the module works in the following way. A
web client (user) requests for a restricted page/directory.
The module sends back a 'Page Has Moved' error, pointing
the client to the page containing a login form. Through
server-side scripting, a session is created in the MySQL
database and the client (i.e. cookies or query string).
The session itself consists of a unique, random, and
temporary ID that is associated with a user. The client
then makes the same request along with the session ID
(SID) and the user ID (UID). The module compares and
validates the two IDs against the IDs stored in the MySQL
database. If successful, the module sends back the requested
page; otherwise, the module once again sends back the
'Page Has Moved' error page. In addition (if specified),
the module will also validate the user's group membership
and act accordingly.
Additional Notes:
1) This module does not verify username and passwords (not
yet). The verification is left up to the web developer via
server-side scripting. For example, if .htpasswd files
are used, a Perl script can be written to verify a user's
input against a .htpasswd file; if the user is verified, the
same Perl script will create a session in the user's browser
(cookies or query string) and in a MySQL database.
2) Do not use Apache's basic authentication directives with
this module (exceptions are 'AuthType Basic' and 'AuthName
"Name"'). Doing so may cause Apache to use basic authentication
whenever a user accesses a restricted page. (See
mod_auth
and similiar modules).
3) To turn off 'mod_auth_form' for certain directories, add
the directive "AuthFormAuthoritative Off" to the directory
container. This is only useful when applying a different
authentication module (e.g. mod_auth) to a directory under
mod_auth_form's control (usually sub-directories).
4) This module requires SELECT permission at minimum. For
'AuthFormSessionTimeout', UPDATE is also required. For
the tracking table, the module also requires INSERT, DELETE,
and UPDATE permissions.
5) Enclose MySQL keywords with backticks (`) when using the 'AuthFormMySQL*'
directives,. For example, "AuthFormMySQLFieldUID SELECT" needs to
be "AuthFormMySQLFieldUID `SELECT`".
6) Use caution when specifying a fixed number of seconds
for AuthFormSessionAutoRefresh. This option does not work
well with inline frames that point to other
auto-refreshing documents. If you have a restricted page
with inline frames, you may need to specify
"AuthFormPageAutoRefresh mypage.html" for each framed
document.
7) 'AuthFormMySQLTableGID' should refer to a join table
if each user is to be associated with multiple groups
(where 'AuthFormMySQLFieldUID' and 'AuthFormMySQLFieldGID'
are the join table attributes). Although this module allows
multiple values for the 'AuthFormMySQLFieldGID' field,
use only one value per record. This multi-value feature
will be removed in the next major version (3.0).
8) This module may be renamed to mod_authnz_form for the next
major version (3.0). The next major version (and maybe the
2.x series) will support authentication via HTTP POST along
with session management, hence the 'nz' after 'auth'.
Case Study 1: A Simple Restricted Area:
A website will contain a restricted area that requires a
valid username and password. The restricted area itself
is a directory called '/restricted'; the login pages for that
restricted directory are '/login.html' and its handler '/login.php'.
The MySQL server resides on the same computer as the Apache server, and
the name of the database is 'users'. To implement this restricted
area, the following must be done:
- httpd.conf
LoadModule auth_form_module modules/mod_auth_form.so
<Directory "/absolute/path/to/restricted">
AuthType Basic
AuthName "Restricted"
# AuthFormMySQLHost # localhost
# AuthFormMySQLPort # Default port (3306)
# AuthFormMySQLUsername # Use Apache's username
# AuthFormMySQLPassword # Password-less login
AuthFormMySQLDB users
AuthFormSessionCookies On
AuthFormPageLogin /login.html
Require valid-user
</Directory>
- MySQL Database: users
- Table: sessions
- Field: VARCHAR (32): sid (PRIMARY KEY)
- Field: VARCHAR (20): uid (FOREIGN KEY)
- Table: passwords
- Field: VARCHAR (20): uid (PRIMARY KEY)
- Field: VARCHAR (32): password (MD5 CRYPTED)
- /login.html
<html>
<head>
...
</head>
<body>
<form action="/login.php" method="post">
<input name="uid" type="text" maxlength="20">
<input name="password" type="password" maxlength="20">
<input type="submit" value="Login">
</form>
</body>
</html>
- /login.php
<?php
function genID($seed, $length)
{
$ID = "";
srand($seed);
for($i = 0; $i < $length; $i++)
{
$chtype = rand(1, 3);
switch($chtype)
{
case 1: // 0-9
$ID .= chr(rand(48, 57));
break;
case 2: // A-Z
$ID .= chr(rand(65, 90));
break;
case 3: // a-z
$ID .= chr(rand(97, 122));
break;
}
}
return $ID;
}
function create_session($mysql, $uid, $password)
{
//
// Build list of existing SIDs
//
$result = $mysql->query("SELECT sid FROM sessions");
$num_rows = $result->num_rows;
while($num_rows > 0)
{
$row = $result->fetch_assoc();
$sids[$row["sid"]] = TRUE;
$num_rows--;
}
$result->close();
//
// Generate SID (making sure it is unique)
//
$max_attempts = 500000;
$seed = crc32($password);
do
{
$sid = genID($seed + time(), 32);
$max_attempts--;
} while(isset($sids[$sid]) && $max_attempts > 0);
if($max_attempts <= 0) // NOT GOOD
return FALSE;
//
// Create the session: set the UID and SID in both the client's cookies and
// the MySQL session table.
//
$mysql->query("INSERT INTO sessions (sid, uid) VALUES ('$sid', '$uid')");
setcookie("uid", $uid, time() + 964224000);
setcookie("sid", $sid, time() + 964224000);
return TRUE;
}
$uid = $_POST["uid"];
$password = $_POST["password"];
$mysql = new mysqli("localhost", "my_username", "my_password", "users");
$result = $mysql->query("SELECT password FROM passwords WHERE uid='$uid'");
$row = $result->fetch_assoc();
$real_password = $row["password"];
$result->close();
if(md5($password) == $real_password)
{
create_session($mysql, $uid, $real_password);
header("Location: /restricted");
}
$mysql->close();
?>
Case Study 2: A Complex Member's Area:
A website will contain a member's area that requires a valid username and password.
The member's area itself is a directory called '/members'; furthermore, there are
two more restricted directories called '/members/paying', which is for paying members
only (where as '/members' is for any member) and '/members/paying/premium', which is
for buisness customers and/or premium members. For added security, all sessions will
expire either from 30 minutes of inactivity or 8 hours from the time of login; also,
sessions are valid if two SID cookies matches and either the UID cookie or the directory's signature
matches (in case the UID cookie is rejected). The login pages for the member's area
are '/member_login.html' and its handler '/member_login.php'. The MySQL server resides
on a host called 'fake_server.com', and the name of the database is 'members'. Also,
the MySQL server will keep tracking records of each request made within the member's area.
For additional complexity, another directory called '/administration' will be accessed
locally by the website maintainers. This directory will use basic authentication from
'mod_auth' using a password file located at '/absolute/path/to/passwords'.
To implement this member's area, the following must be done:
- httpd.conf
LoadModule auth_form_module modules/mod_auth_form.so
<Directory "/absolute/path/to/members">
AuthType Basic
AuthName "Member's Area"
AuthFormMySQLHost fake_server.com
AuthFormMySQLUsername my_username
AuthFormMySQLPassword my_password
AuthFormMySQLDB members
AuthFormMySQLTableGID uid_gid
AuthFormMySQLTableSIDCondition "`sid`=$sid1 AND `sid_dir`=$sid2\
AND (`uid`=$uid OR `signature`='some_signature_members')"
AuthFormMySQLTableTracking tracking
AuthFormMySQLFieldExpiration expiration_date
AuthFormLastPageKey go_back_to
AuthFormSessionTimeout 30 #minutes
AuthFormSessionCookies On
AuthFormPageLogin /member_login.html
AuthFormPageExpired /session_expired.html
Require group 0 1 #non-paying paying
</Directory>
<Directory "/absolute/path/to/members/paying">
AuthFormPageNotAllowed /paying_members_only.html
AuthFormMySQLTableSIDCondition "`sid`=$sid1 AND `sid_dir`=$sid2\
AND (`uid`=$uid OR `signature`='some_signature_paying')"
Require group 1 #paying
</Directory>
<Directory "/absolute/path/to/members/paying/premium">
AuthFormPageNotAllowed /premium_only.html
Require group 2 #premium
</Directory>
<Directory "/absolute/path/to/administration">
AuthType Basic
AuthName "Administration"
AuthUserFile /absolute/path/to/passwords
# Turn off 'mod_auth_form' in this directory,
# giving control to 'mod_auth'.
AuthFormAuthoritative Off
Allow from 127.0.0.1 # localhost
Deny from all
Order Deny,Allow
Require valid-user
</Directory>
- MySQL Database: members
- Table: sessions
- Field: VARCHAR (32): sid (PRIMARY KEY)
- Field: VARCHAR (32): sid_dir
- Field: INT (8) UNSIGNED: uid (FOREIGN KEY)
- Field: VARCHAR (20): signature
- Field: DATETIME: timeout_date
- Field: DATETIME: expiration_date
- Table: creds
- Field: INT (8) UNSIGNED: uid (PRIMARY KEY)
- Field: VARCHAR (20): username (UNIQUE)
- Field: VARCHAR (32): password_md5
- Other fields describing each user...
- Table: groups
- Field: INT (8) UNSIGNED: gid (PRIMARY KEY)
- Field: VARCHAR (20): groupname
- Other fields describing each group...
- Table: uid_gid
- Field: INT (8) UNSIGNED: uid (FOREIGN KEY)
- Field: INT (8) UNSIGNED: gid (FOREIGN KEY)
- Table: tracking
- Field: INT (8) UNSIGNED: uid (FOREIGN KEY)
- Field: VARCHAR (15): client_ip_address
- Field: DATETIME: download_date
- Field: VARCHAR (255); download_path
- Field: INT (8) UNSIGNED: download_size
- /member_login.php
<?php
//
// Assume the function 'genID' from the previous case study is defined.
//
function create_session($mysql, $uid, $password)
{
//
// Build list of existing SIDs
//
$result = $mysql->query("SELECT sid FROM sessions");
$num_rows = $result->num_rows;
while($num_rows > 0)
{
$row = $result->fetch_assoc();
$sids[$row["sid"]] = TRUE;
$num_rows--;
}
$result->close();
//
// Generate main SID (making sure it is unique)
//
$max_attempts = 500000;
$seed = crc32($password);
do
{
$sid1 = genID($seed + time(), 32);
$max_attempts--;
} while(isset($sids[$sid1]) && $max_attempts > 0);
if($max_attempts <= 0) // NOT GOOD
return FALSE;
$sid2 = genID(crc32($sid1) + time(), 32);
//
// Create the session: set the UID and SID in both the client's cookies and
// the MySQL session table.
//
$mysql->query("INSERT INTO sessions (sid, sid_dir, uid, signature, timeout_date, expiration_date)
VALUES ('$sid1', '$sid2', '$uid', 'some_signature_members', DATE_ADD(NOW(), INTERVAL 30 MINUTE),
DATE_ADD(NOW(), INTERVAL 8 HOUR))");
setcookie("sid1", $sid1, time() + 964224000);
setcookie("sid2", $sid2, time() + 964224000);
setcookie("uid", $uid, time() + 964224000);
return TRUE;
}
$username = $_POST["username"];
$password = $_POST["password"];
$mysql = new mysqli("fake_server.com", "my_username", "my_password", "members");
$result = $mysql->query("SELECT uid,password_md5 FROM creds WHERE username='$username'");
$row = $result->fetch_assoc();
$uid = $row["uid"];
$real_password = $row["password_md5"];
$result->close();
if(md5($password) == $real_password)
{
create_session($mysql, $uid, $real_password);
header("Location: /members");
}
$mysql->close();
?>
Logging Out:
Besides session expiration, logging out of a session is done simply by deleting the
session's record from the MySQL database. In addition, the client's cookies can be
expired if cookies are used. Here is a PHP script for destroying a session, assuming
the client passes the session cookies.
<?php
$uid = $_COOKIE["uid"];
$sid = $_COOKIE["sid"];
$mysql = new mysqli("mysql_host", "my_username", "my_password", "my_database");
$mysql->query("DELETE FROM sessions WHERE sid='$sid'");
$mysql->close();
setcookie("uid", $uid, time() - 964224000);
setcookie("sid", $sid, time() - 964224000);
header("Location: login_page");
?>
Authoritative
Flag : DEFAULT="On"
Turn on 'mod_auth_form'.
LastPageKey (version 2.02+)
String : OPTIONAL
The name of the url query string key containing
the URL that a client unsuccessfully accessed
(i.e. the URL from which the client was redirected).
MySQLDB
String : REQUIRED
The MySQL database to connect to.
MySQLFieldDownloadDate
String : DEFAULT="download_date"
Field under the tracking table that stores
the time of request.
MySQLFieldDownloadPath
String : DEFAULT="download_path"
Field under the tracking table that stores
the path of request.
MySQLFieldDownloadSize
String : DEFAULT="download_size"
Field under the tracking table that stores
the size of request.
MySQLFieldExpiration
String : OPTIONAL
Field under the session table that stores
the time the session will expire regardless
of user activity. Not specifying this configuration
disables session expiration (although session inactivity
timeout can still be enabled). Also,
AuthFormPageExpired must be configured.
MySQLFieldGID
String : DEFAULT="gid"
Field under the group table that stores
the user's space/comma delimited list
of group IDs.
NOTE: Having a multi-value
field violates the conceptual design of relational
databases. In the next major version (3.0), this
feature will be removed in favor of a join table.
MySQLFieldIPAddress
String : DEFAULT="client_ip_address"
Field under the tracking table that stores
the client's IP address.
MySQLFieldTimeout
String : DEFAULT="timeout_date"
Field under the session table that stores
the time the session will expire if the user
is inactive.
MySQLFieldUID
String : DEFAULT="uid"
Field under the session, group, and tracking
tables that stores the user ID.
MySQLHost
String : DEFAULT="localhost"
The fully-qualified name or IP address of the
MySQL server.
MySQLPassword
String : DEFAULT=<Blank Password>
The MySQL user's password.
MySQLPort (version 2.03+)
Number : DEFAULT="3306"
The MySQL port to connect to.
MySQLSSL (version 2.04+)
Flag : DEFAULT="Off"
Use SSL connections to MySQL. Please see the
AuthFormMySQLSSL* directives. Those directives
mimic the parameters in mysql_ssl_set() from the
MySQL Client API.
MySQLSSLCA (version 2.04+)
String : OPTIONAL
The path to the file listing the trusted certificate
authorities.
MySQLSSLCAPath (version 2.04+)
String : OPTIONAL
The path to the directory containing the PEM-formatted,
trusted certificate authorities.
MySQLSSLCert (version 2.04+)
String : OPTIONAL
The path to the MySQL client certificate.
MySQLSSLCipherList (version 2.04+)
String : DEFAULT="!ADH:RC4+RSA:HIGH:MEDIUM:LOW:EXP:+SSLv2:+EXP"
The list of SSL ciphers (in 'openssl ciphers' format) to
allow for SSL connections.
MySQLSSLKey (version 2.04+)
String : OPTIONAL
The path to the MySQL client certificate key.
MySQLSocket (version 2.04+)
String : OPTIONAL
The path to the MySQL server socket file
(Unix only).
MySQLTableGID
String : OPTIONAL
The table that stores the group memberships.
AuthFormMySQLFieldGID and
AuthFormPageNotAllowed must also be configured
along with the
Require directive in
a directory container (refer the the
Apache2 documentation for more information).
Also, the table must have a UID and GID field where
the UID field is the primary key.
MySQLTableGIDCondition
String : OPTIONAL
Condition to add to the WHERE-clause when
querying the group membership table.
MySQLTableSID
String : DEFAULT="sessions"
The table that stores session records. At
minimum, the table should have a SID field and
a UID field where the SID field is the primary key.
MySQLTableSIDCondition
String : DEFAULT="sid=$sid AND uid=$uid"
Session validation condition used in the WHERE-clause
when querying the session table. The variable
placeholders (denoted as $var) store session values
passed by the clients and validated by the module.
With the exception of the placeholders, the syntax
of the condition is the same as the syntax in
a MySQL WHERE clause.
MySQLTableTracking
String : OPTIONAL
The table that stores request tracking information.
The table must have a field for UID, client's IP
address, download date, download path, and download
size (no primary keys).
MySQLTableTrackingCondition
String : OPTIONAL
Condition to add to the WHERE-clause when
querying the tracking table.
MySQLUsername
String : DEFAULT=<Apache2 Username>
The MySQL user used to connect to the MySQL
server.
PageAutoRefresh (version 2.02+)
String : OPTIONAL
The URL to which to auto-refresh.
Effectively speaking, this directive defaults
to the current page if session expiration is
disabled and/or not used (see
AuthFormSessionAutoRefresh). Otherwise,
this directive defaults to AuthFormPageExpired.
PageExpired
String : OPTIONAL
The URL to the 'session expired' page.
PageLogin
String : REQUIRED
The URL to the page containing the login form.
PageNotAllowed
String : OPTIONAL
The URL to the 'invalid group member' page.
SessionAutoRefresh (version 2.02+)
Number : DEFAULT=-1
How many seconds should the web browser refresh
to a certain page (if not the current page).
A value of 0 means disable auto-refreshing.
A value of -1 (default) means auto-refresh when
the client's session expires; if session
expiration is disabled, auto-refreshing will
effectively be disabled.
Any value greater than 0 indicates a fixed
number of seconds to auto-refresh.
See also AuthFormPageAutoRefresh.
SessionCookies
Flag : DEFAULT="Off"
Whether to use cookies or the URL query string
to pass the session keys from the client to the
module. ('On' means use cookies; 'Off' means
use the URL query string).
SessionDelete (version 2.03+)
Flag : DEFAULT="Off"
Whether or not the module should delete expired
sessions per request.
NOTE: This feature is not as robust as
managing expired sessions via
server-side scripting, especially when
auto-refreshing is used.
SessionTimeout
Number : DEFAULT=0
The session inactivity timeout in minutes. A
value of '0' indicates no timeout. Also,
AuthFormPageExpired must be configured.
TrackingLifetime
Number : DEFAULT=30
Maximum number of days to hold a tracking record.
A value of '0' indicates infinite lifetime.
(The module goes by the date of download field under
the tracking table).