I program for a long time in other languages. So I don't think it is perfect but maybe this post can trigger a little discussion on
what to improve as well.
I know many that are new to PHP are in need of an authentication module. I will explain certain steps to a working module
and then add more functionallity to it. In the end you will have an authentication module that can do the following:
- Basic WWW-Authentication
- Session based access timing (logging users off automatically after 15 minutes)
- logging of multiple failed attempts and refusing access based on Session, Username, IP
- Functionality with Cookies enabled and disabled even if trans id is turned off.
Here a quick legal note: If you use this code use it on your own risk. Don't blame me if anything goes wrong.
Requirements:
- *nix or Linux system with Apache webserver, PHP and MySQL
- I believe WWW-Authentication only works if PHP is compiled into Apache. If you use PHP as CGI you are probably out of luck.
I'am not sure about a dynamicly loaded php apache module but maybe one of you can enlighten us on that.
- A mind that ignores any spelling errors or typos.
Ok here we go:
First we create some basic functions:
1. A MySQL database connection function:
Code: Select all
<?php
function connect()
{
mysql_connect("localhost", "username", "password") or die("No connection to MySQL Server");
mysql_select_db("dbname") or die("Can't select database ".mysql_error() . ") ");
}
?>2. The WWW-Authentication function
Code: Select all
<?php
function authenticate()
{
Header('WWW-authenticate: Basic realm="Any_name"');
Header("HTTP/1.0 401 Unauthorized");
include("$_SERVER[DOCUMENT_ROOT]/../inc/denied.php");
exit;
}
?>After the user types in the username and password the whole document is recalled. This is very important for the understanding of this
function. If you include the authentication module in e.g. index.php, index.php is recalled from the beginning every time you call this function.
3. The function to verify the user against a MySQL database
Code: Select all
<?php
function verify_user($user, $pass)
{
$pw_selstr = "SELECT user, nick, pass_word FROM verify_user WHERE (user='$user' or nick='$user' ) AND pass_word=password('$pass')";
$just-a-name = mysql_query($pw_selstr);
if (!$just-a-name) die("<br><b>$PHP_SELF</b>: ".mysql_error());
else if(!mysql_num_rows($just-a-name)) {
authenticate();
} else return $just-a-name;
}
?>Code: Select all
The MySQL table I am using is following:
user mediumint(8)
nick char(15)
pass_word char(20)
primary key is user. I placed indexes on all fields.This searches for a username or a nickname with the password in your database. Many times users get a userid when they sign up and later are able
to choose a nickname as well. I wrote this funtion so both userid and nickname are valid usernames. If you just want to use username just change
the Select string and table as it suits your needs. It calls authenticate() when the querry returns no entry.
Now following code completes the very basic authentication module.
Code: Select all
<?php
connect();
$user = mysql_escape_string($_SERVER['PHP_AUTH_USER']);
$pass = mysql_escape_string($_SERVER['PHP_AUTH_PW']);
if (!isset($user)) authenticate();
else {
if ($result=verify_user($user, $pass)) {
// successfully logged in
// place any or no code here as this is the only way out of this module
}
else {
authenticate();
}
}
?>The disadvantage of this module is that if the user leaves the browser open anyone using that browser can access the members area. This might
be acceptable for members areas where only content is displayed but if personal data is involved this might just not be enough.
So we will add a session to this module. The following code is placed at the beginning of the module.
Code: Select all
<?php
session_set_cookie_params(0, '/', '.foo.com');
@session_start();
?>over different subdomains as www. or secure. This setting should always be there if you use different servers. Otherwise you might just drop it.
The @ is placed in fromt of the session handler to avoid nasty error messages if someone tampers with the session_id().
Now we are adding the code to check if the user has not done anything (link, reload etc.) in 15 minutes and reauthorize if that is the case.
This code is placed just in front of --> if (!isset($user)) authenticate();
Code: Select all
<?php
$timeout = time() - 900;
$del_str = "DELETE FROM Sess_Log WHERE TStamp < '$timeout'";
$just-a-name = @mysql_query($del_str);
$opt_str = "OPTIMIZE TABLE `Sess_Log` ";
$just-a-name = @mysql_query($opt_str);
if (ereg('^[A-Za-z0-9]{32}$', session_id())) {
$sess = session_id();
} else {
$_SESSION = array();
@session_destroy();
session_set_cookie_params(0, '/', '.foo.com');
@session_start();
}
$sess_str = "SELECT * FROM Sess_Log WHERE session = '$sess'";
$just-a-name = @mysql_query($sess_str);
if (mysql_num_rows($just-a-name) > 0) {
$just-a-time = time();
$upd_str = "UPDATE Sess_Log SET time_stamp = '$just-a-time', username = '$user' WHERE session = '$sess'";
$just-a-name = @mysql_query($upd_str);
} else {
$addressip = mysql_escape_string($_SERVER['REMOTE_ADDR']);
$sess = mysql_escape_string(session_id());
$just-a-time = time();
$log_str = "INSERT INTO Sess_Log (ip_address, session, time_stamp, username) VALUES ('$addressip', '$sess', '$just-a-time', '$user')";
$just-a-name = @mysql_query($log_str);
authenticate();
}
?>Code: Select all
MySQL table:
time_stamp int(11)
session char(32)
ip_address char(15)
username char(15)
Primary is session but you might as well don't use any primary. Indexes on all fields.First we clear the Sess_Log table of all entries older than 15 minutes. As this is done quite often we will optimize that table right away as well.
Next we validate the session_id and if anything is invalid we destroy the old session and start a new one.
Next we check if there is an entry in the database with the current session_id. If it is found we update the timestamp and the username. This is done
as the username might be empty on the first call. So we update this as soon as the the authentication was done.
If no entry is found we create a new entry and call authenticate().
This works fine if cookies are enabled or trans-id is enabled but I promised you a solution for disbaled cookies and disabled trans-id so here we go.
We adjust the else branch of above code to the following:
Code: Select all
<?php
$addressip = mysql_escape_string($_SERVER['REMOTE_ADDR']);
$sess_str = "SELECT session, time_stamp, username, ip_address FROM Sess_Log WHERE username = '$user' AND ip_address= '$addressip'";
$just-a-name = @mysql_query($sess_str);
if (mysql_num_rows($just-a-name) > 0) {
// wenn ja dann updaten und durchlaufen lassen bis zur nutzerverifizeirung
$just-a-time = time();
$sess = mysql_escape_string(session_id());
$upd_str = "UPDATE Session_Log SET TStamp = '$just-a-time', Session = '$sess' WHERE User = '$user' AND IP_Address= '$addressip'";
$just-a-name = @mysql_query($upd_str);
} else {
$sid_id = strip_tags(SID);
if (empty($sid_id) ) {
if ($_SERVER['QUERY_STRING'] <> '') {
$urlfield = explode("?", $_SERVER['REQUEST_URI']);
header("Location: http://www.foo.com$urlfield[0]");
exit();
}
} else {
//wenn nein dann überprüfen ob query string übergeben wurde
if (!$_SERVER['QUERY_STRING'] <> '') {
$blib = $_SERVER['REQUEST_URI'];
header("Location: http://www.foo.com".$blib."?".$sid_id);
exit();
}
}
$addressip = mysql_escape_string($_SERVER['REMOTE_ADDR']);
$sess = mysql_escape_string(session_id());
$just-a-time = time();
$log_str = "INSERT INTO Sess_Log (ip_address, session, time_stamp, username) VALUES ('$addressip', '$sess', '$just-a-time', '$user')";
$just-a-name = @mysql_query($log_str);
authenticate();
}
?>update that database entry and this branch is done. Now we do one more check to even get the users coming in with proxy isp's and changing ip's.
We will now check if SID is empty. SID is empty if a valid session cookie got back from the user. Otherwise it displays 'sessionname=session_id'. Now we have the
problem that we have a session_id to place into the database but as we authenticate a new session id is generated and we would recall authenticate over and
over. So we need to place the SID into the URL for the authenticate siterecall so we can place a valid entry into Sess_Log for users that come in with proxy isp's.
Of course this will be also done for a user with cookies enabled that has a bookmark on the members-area. So on the second call we get rid of the not needed
part of the URL and recall again. This might not be the most elegant way but it does the job. Of course a proxy isp user would have to reauthenticate on every
link he clicks in the members area if his ip changed but well he decided to turn cookies off. I like this way better than just telling him to turn cookies on as it enables
him to use the members area with cookies disabled but with lot of inconvenience
Please note that I haven't included the url validation in this part but it should be done.
Now we do have a functioning authentication module even for users that don't use cookies. We will now add another funtion to the code to log failed attempts and
block access after a couple of failed attempts. It will block multiple attempts on the same username and multiple usernames with the same ip or session.
Code: Select all
<?php
function log_failed_user($user) {
if (!empty($user)) {
$timeout = time() - 300;
$del_str = "DELETE FROM Fail_Log WHERE time_stamp < '$timeout'";
$just-a-name = @mysql_query($del_str);
$opt_str = "OPTIMIZE TABLE `Fail_log` ";
$just-a-name = @mysql_query($opt_str);
$addressip = mysql_escape_string($_SERVER['REMOTE_ADDR']);
$sess = mysql_escape_string(session_id());
$log_str = "SELECT username, ip_address, session, time_stamp FROM Fail_Log WHERE username='$user' OR ip_address = '$addressip' OR session = '$sess'";
$just-a-name = @mysql_query($log_str);
if ($just-a-name) {
if (mysql_num_rows($just-a-name) > 3) {
header('Location: http://www.foo.com/blocked.php');
exit();
}
}
$just-a-time = time();
$log_str = "INSERT INTO Fail_Log (ip_address, session, time_stamp, username) VALUES ('$addressip', '$sess', '$just-a-time', '$user')";
$just-a-name = @mysql_query($log_str);
}
}
?>Code: Select all
MySQL table
time_stamp int(11)
username char(15)
ip_dddress char(16)
session char(32)This basically deletes all expired (5 minutes old) entries and optimizes the table to always have the best performance. Checks for more than 3 entries and calls a
php page that displays the blocked message. If 3 or less are found a new entry is written. This will avoid that the MySQL table gets big due to a brute force
attack as the blocked site is called and no entry is written.
Now we just place the function call
Code: Select all
<?php
log_failed_user($user);
?>Here is the complete code
Code: Select all
<?php
function connect()
{
mysql_connect("localhost", "username", "password") or die("No connection to MySQL Server");
mysql_select_db("dbname") or die("Can't select database ".mysql_error() . ") ");
}
function authenticate()
{
Header('WWW-authenticate: Basic realm="Any_name"');
Header("HTTP/1.0 401 Unauthorized");
include("$_SERVER[DOCUMENT_ROOT]/../inc/denied.php");
exit;
}
function verify_user($user, $pass)
{
$pw_selstr = "SELECT user, nick, pass_word FROM verify_user WHERE (user='$user' or nick='$user' ) AND pass_word=password('$pass')";
$just-a-name = mysql_query($pw_selstr);
if (!$just-a-name) die("<br><b>$PHP_SELF</b>: ".mysql_error());
else if(!mysql_num_rows($just-a-name)) {
authenticate();
} else return $just-a-name;
}
function log_failed_user($user) {
if (!empty($user)) {
$timeout = time() - 300;
$del_str = "DELETE FROM Fail_Log WHERE time_stamp < '$timeout'";
$just-a-name = @mysql_query($del_str);
$opt_str = "OPTIMIZE TABLE `Fail_log` ";
$just-a-name = @mysql_query($opt_str);
$addressip = mysql_escape_string($_SERVER['REMOTE_ADDR']);
$sess = mysql_escape_string(session_id());
$log_str = "SELECT username, ip_address, session, time_stamp FROM Fail_Log WHERE username='$user' OR ip_address = '$addressip' OR session = '$sess'";
$just-a-name = @mysql_query($log_str);
if ($just-a-name) {
if (mysql_num_rows($just-a-name) > 3) {
header('Location: http://www.foo.com/blocked.php');
exit();
}
}
$just-a-time = time();
$log_str = "INSERT INTO Fail_Log (ip_address, session, time_stamp, username) VALUES ('$addressip', '$sess', '$just-a-time', '$user')";
$just-a-name = @mysql_query($log_str);
}
}
session_set_cookie_params(0, '/', '.foo.com');
@session_start();
connect();
$user = mysql_escape_string($_SERVER['PHP_AUTH_USER']);
$pass = mysql_escape_string($_SERVER['PHP_AUTH_PW']);
$timeout = time() - 900;
$del_str = "DELETE FROM Sess_Log WHERE TStamp < '$timeout'";
$just-a-name = @mysql_query($del_str);
$opt_str = "OPTIMIZE TABLE `Sess_Log` ";
$just-a-name = @mysql_query($opt_str);
if (ereg('^[A-Za-z0-9]{32}$', session_id())) {
$sess = session_id();
} else {
$_SESSION = array();
@session_destroy();
session_set_cookie_params(0, '/', '.foo.com');
@session_start();
}
$sess_str = "SELECT * FROM Sess_Log WHERE session = '$sess'";
$just-a-name = @mysql_query($sess_str);
if (mysql_num_rows($just-a-name) > 0) {
$just-a-time = time();
$upd_str = "UPDATE Sess_Log SET time_stamp = '$just-a-time', username = '$user' WHERE session = '$sess'";
$just-a-name = @mysql_query($upd_str);
} else {
$addressip = mysql_escape_string($_SERVER['REMOTE_ADDR']);
$sess_str = "SELECT session, time_stamp, username, ip_address FROM Sess_Log WHERE username = '$user' AND ip_address= '$addressip'";
$just-a-name = @mysql_query($sess_str);
if (mysql_num_rows($just-a-name) > 0) {
// wenn ja dann updaten und durchlaufen lassen bis zur nutzerverifizeirung
$just-a-time = time();
$sess = mysql_escape_string(session_id());
$upd_str = "UPDATE Session_Log SET TStamp = '$just-a-time', Session = '$sess' WHERE User = '$user' AND IP_Address= '$addressip'";
$just-a-name = @mysql_query($upd_str);
} else {
$sid_id = strip_tags(SID);
if (empty($sid_id) ) {
if ($_SERVER['QUERY_STRING'] <> '') {
$urlfield = explode("?", $_SERVER['REQUEST_URI']);
header("Location: http://www.foo.com$urlfield[0]");
exit();
}
} else {
//wenn nein dann überprüfen ob query string übergeben wurde
if (!$_SERVER['QUERY_STRING'] <> '') {
$blib = $_SERVER['REQUEST_URI'];
header("Location: http://www.foo.com".$blib."?".$sid_id);
exit();
}
}
$addressip = mysql_escape_string($_SERVER['REMOTE_ADDR']);
$sess = mysql_escape_string(session_id());
$just-a-time = time();
$log_str = "INSERT INTO Sess_Log (ip_address, session, time_stamp, username) VALUES ('$addressip', '$sess', '$just-a-time', '$user')";
$just-a-name = @mysql_query($log_str);
authenticate();
}
}
if (!isset($user)) authenticate();
else {
if ($result=verify_user($user, $pass)) {
// successfully logged in
// place any or no code here as this is the only way out of this module
}
else {
authenticate();
}
}
?>I hope that you guys find any flaws in this code so I can make it better. Otherwise I hope you have gotten something out of this tutorial.
I am happy to get any improvements and feedback as I cannot be sure how all kinds of browsers might behave to this module.