User identification using cookies in PHP/MySQL
What is a cookie and how do we use them in PHP. If you can not answer this question, you should definitely have a read through this article.
This
article will explain the basics and possibilities of user
identification using cookies. First of all the mechanics of cookies
must be explained in order to understand what can be passed to cookies
and how well protected that information is.
Cookies
are ASCII files that browsers store in temporary internet directories.
These files are set by internet webpage, either through HTTP header
properties, or by using javascript code. Therefore, the browser is the
one responsible of storing the cookie files, by request of the viewed
page. All that gets transmitted from remote page is a set of variables
the cookie should contain. A cookie contains several variables that are
important to us:
- name string
- information string, called VALUE
- expiration time
- relevant host address
- relevant host directory
Name is cookie’s name. By this name it will be identified in our script.
The
value is a string that can carry certain information. In our case, it
will hold the data user is identified by. Expiration time is number of
seconds passed since January 1., 1970., midnight GMT. Therefore, it
represents accurate date/time combination when the cookie is no longer
valid. The host address identifies the host this cookie will be
transmitted to, and the directory represents host’s subdirectory which
should receive this cookie.
In
other words, by setting the address and directory you somewhat ensure
that only files contained within that directory and on that host will
receive the cookie. For example, you have a script called identify.php
with the following url:
www.somedomain.com/somedirectory/identify.php
To
ensure only identify.php will receive the desired cookie, the cookie’s
host address must be “.somedomain.com” and the directory must be
“/somedirectory/”. Browsers will recognize the addres/directory
combination and will pass the cookie to the identify.php script. Note
that domain address has a dot as the first character. This ensures that
any prefix validates the domain, prefixes being ‘www’ or ‘ftp’ or any
other.
So the mechanism is
this: browsers register domains and directories for all cookies stored
in a local directory. When the browser is directed to a particular URL,
only the cookies that are registered for that URL (domain/directory
combination) will be passed to a HTML or PHP file in that URL, along
with all those cookies that do not have domain address nor host
directory set (which are in that case passed to all URL’s browsers are
directed to). Also, only those cookies that have not yet expired will
be passed. Expired cookies will be deleted from local computers by the
browser.
This article assumes that the reader is familiar with PHP programming and usage of MYSQL databases.
The Article
Setup of Cookie Data
Up
to now we know what a cookie can and should contain. In order to
strenghten the security, domain address and relevant subdirectories
must always be set, along with expiration time.
Before
we actually deal with the data our cookie should carry, we must define
user data first. Let’s assume that our users have following fields in
the database:
- ID
- Username
- Password
- Logcode
along with any other data your users list must contain.
Let us set up the MySQL table with a following command:
>CREATE TABLE my_users (
>id INT UNSIGNED NOT NULL AUTO_INCREMENT,
>username VARCHAR(20),
>password CHAR(32),
>logcode VARCHAR(32),
>PRIMARY KEY (id),
>INDEX (username),
>INDEX (password));
Certain fields require explanation:
password will be a md5 hash of user’s password, so it is 32 chars fixed-length
logcode is a md5 hash that identifies whether the user is logged in or not.
Now, let us explain what data will be transmited to cookies, and in what manner.
- When the user logs in, he is identifed by his username/password combination (thus the indices).
- Upon actual login he will receive a randomly created, md5 hashed code called logcode
- The logcode will be written in my_users table, for that particular user
- User’s ID and LOGCODE will be set in a cookie, on user’s computer
When
the user is accessing any protected area, a small function should read
the cookie and compare if ID exists in the table, and if does, compare
to see if LOGCODEs are equal. If they are not, the user is not allowed
to access the page, if they are, a new LOGCODE is generated, stored in
the table, and stored on user’s computer in the cookie.
Therefore,
upon each successful access, the user receives new LOGCODE, which
actually updates his logged status, and strenghtens the security. We
will see later why.
Now, let us see what should actually happen in our scripts.
login.php and Logging of Users
A
small form should input user’s username and password, both passed to
login.php via POST protocol. The login.php does the following:
<?php
// Check if the script received required values
if (isset($_POST['username']) && isset($_POST['password'])) {
// trim and read username and password from the form
$username= ltrim(rtrim(addslashes($_POST['username'])));
$password= ltrim(rtrim(addslashes($_POST['password'])));
// generate a MD5 hash of the password
$mdpass= md5($password);
// now, detect user (get his ID) by username/password_md5 combination
$res=
mysql_query("SELECT id FROM my_users WHERE username='$username' AND
password='$mdpass'") or die(“Could not select user ID.”);
// just to ensure there is only one user by that username/password combination
// in the database, we check if only one row was returned
if (mysql_num_rows($res)==1) {
$user_obj= mysql_fetch_object($res);
$user_id= $user_obj->id;
// now generate a random 8 char long string, and hash it with MD5
$logcode= md5(func_generate_string());
// now update user’s information in the database
$res= mysql_query("UPDATE my_users SET logcode='$logcode' WHERE id=$user_id") or die(“Could not update database.”);
// now, let us setup the identification information that will be passed to user’s computer via a cookie
// we will store user’s ID and LOGCODE in ID:LOGCODE form so that we can later extract it using explode() function
$newval= "$user_id:$logcode";
// store the cookie
setcookie("cookiename", $newval, time() + 300, "/subdirectory/", ".somedomain.com");
// redirect to some user welcome area
header("Location: http://www.somedomain.com/subdirectory/welcome.php");
exit;
} else {
// report invalid user or invalid username/password combination
}
} else {
// report invalid login
}
?>
Now,
it must be mentioned that cookies can only be set in HTTP headers,
thatis before any output has been generated for browsing. Our die()
functions will abort before the cookie is set, automatically switching
to HTML output mode, setcookie will never be reached in that case.
Lastly, we redirect to a welcome area. This is important, because set
cookies are not visible until next reload of some page, under that
domain and subdirectory. Also, subdirectories are not needed. You can
have your script in domain’s root, in which case you omit the directory
value. See PHP documentation for setcookie() function explanation.
Note
that this script requires an open connection to MySQL database, so you
must open it before abovementioned code, and close the connection after.
The Random String Generating Function
As
seen in previous caption, we have generated a 8 char long random string
that we additionally hashed with MD5 algorithm. First, here is the code
of func_generate_string()
<?php
function func_generate_string() {
$auto_string= chr(mt_rand(ord('A'), ord('Z')));
for ($i= 0; $i<8; $i++) {
$ltr= mt_rand(1, 3);
if ($ltr==1) $auto_password .= chr(mt_rand(ord('A'), ord('Z')));
if ($ltr==2) $auto_password .= chr(mt_rand(ord('a'), ord('z')));
if ($ltr==3) $auto_password .= chr(mt_rand(ord('0'), ord('9')));
}
return $auto_string;
}
?>
What
this function actually does is that it sets the first character to
random uppercase letter (A-Z), and then it sets the following
characters to either uppercase (A-Z; if $ltr=1), lowercase (a-z; if
$ltr=2) or a number (0-9; if $ltr=3). Using mt_rand() function we
strenghten the random distribution. The for(;;) loop can be modified to
output any number of characters.
This
is a neat function that can be used in automatic password generation
too, or any other situation that requires a random string.
detectuser.php and User Identification
The
next script actually identifies the user, via values in cookies. It is
important to note that this script must be required with require()
function in all your pages that require logged users. Also, this script
will setup a global $global_user_id variable that stores user’s id in
my_users table. You can use its value to manipulate user’s data or
monitor what the user is doing. Also, require detectuser.php at the
beginning of your scripts to ensure eventual header redirections before
anything is output. An example for this would be if the user is not
identified, but has requested a protected page, the page will redirect
to a login page, instead of showing its contents.
someprotectedpage.php
<?php
$legal_require_php= 1234;
require (‘detectuser.php’);
...
?>
detectuser.php
<?php
//see if detectuser.php has been required, not URL’d.
if ($legal_require_php!=1234) exit;
// setup global variable $global_user_id, set it to 0, which means no user as auto_increment IDs in MySQL begin with 1
$global_user_id= 0;
// now, check if user’s computer has the cookie set
if (isset($_COOKIE['cookiename'])) {
$cookieval= $_COOKIE['cookiename'];
//now parse the ID:LOGCODE value in cooke via explode() function
$cookieparsed= explode (":", $cookieval);
// $cookie_uid will hold user’s id
// $cookie_code will hold user’s last reported logcode
$cookie_uid= $cookieparsed[0];
$cookie_code= $cookieparsed[1];
// ensure that ID from cookie is a numeric value
if (is_numeric($cookie_uid)) {
//now, find the user via his ID
$res= mysql_query("SELECT logcode FROM my_users WHERE id=$cookie_uid");
// no die() this time, we will redirect if error occurs
if ($res) {
// now see if user’s id exists in database
if (mysql_num_rows($res,0) {
$logcode_in_base= mysql_result($res, 0);
// now compare LOGCODES in cookie against the one in database
if ($logcode_in_base == $cookie_code) {
// if valid, generate new logcode and update database
$newcode= md5(func_generate_string());
$res= mysql_query(“UPDATE my_users SET logcode=’$newcode’ WHERE id=$cookie_uid”);
// setup new cookie (replace the old one)
$newval= “$cookie_uid:$newcode”;
setcookie("cookiename", $newval, time() + 300, "/subdirectory/", ".somedomain.com");
// finally, setup global var to reflect user’s id
$global_user_id= $cookie_uid;
} else {
// redirect if logcodes are not equal
}
} else {
// redirect if user ID does not exist in database
}
} else {
// redirect in case of database error
}
} else {
// redirect if user ID in cookie not numeric
}
}
?>
First,
let us explain the $legal_require_php variable. In order to ensure that
our script is actually required(), not called via URL, we must setup
this variable to some arbitrary value in all CALLER scripts. In all
REQUIRED scripts we check if that variable is set and contains chosen
value.
Next, a note on redirection. We redirect from ALL secure pages (all pages that require() detectuser.php) in following cases:
- cookie is not set – meaning, user is not logged in, or cookie expired
- logcode from cookie is not the same as logcode in database – meaning breach attempt
- value ID in cookie is not numeric – someone tampered with the cookie, or the cookie is corrupt
- user does not exist in database – cookie corrupted or has been tampered with
- database error
You can setup redirection or error message notification in mentioned cases. That is up to you.
Also
note that on each access to detectuser.php (meaning on each reload or
click within secure pages) the user receives a new logcode. This renews
user identification and somewhat disables someone else to steal the
cookie (or its value) and sets it on his own computer, pretending to be
a logged user. More frequent the clicks (reloads of detectuser.php),
more secure he is from being hacked.
Also,
we have set up the cookie expiration time to be 5 minutes (300 seconds)
from current access time. This means the user will be considered logged
in for up to 5 minutes of him being idle. You can modify this number,
or request your users to choose this time upon registration. By storing
this time (in seconds) in the my_users table, you can, upon each
access, read it and set it to cookie expiration time like this:
$expire= time() + $user_chosen_time_in_seconds;
setcookie("cookiename", $newval, $expire, "/subdirectory/", ".somedomain.com");
The logout.php and Cookie Cleanup
When
the user wants to logout, all that has to be done (in a separate
script, if wished) is to store an empty cookie with SAME NAME and
host/directory parameters, but with expiration time set in past.
In
order for the script to know who is logged out, pass the
$global_user_id value to it, preferably via POST protocol. This means
you should have a LOGOUT button in a small form, somewhere on your
secure pages. Pass the user id via userid variable, as hidden value in
your form.
<?php
// ensure the userid is passed
if (isset($_POST['userid'])) {
$userid= $_POST['userid'];
// ensure the value is numeric
if (is_numeric($userid)) {
// update database
$res= mysql_query("UPDATE my_users SET logcode='none' WHERE id=$userid");
setcookie("cookiename", "empty", time() - 3600, "/directory/", ".somedomain.com");
}
}
// redirect to a logoff (thanks for using our service) page
header("Location: http://somedomain.com/logoffpage.php");
exit;
?>
As
you can see, we have set the cookie with the same name and parameters,
but with empty value string, and expiration time somewhere in the past
(one hour before now).
If
someone misuses this script and somehow passes some value to this
script, all he can do is to logout the user with ID he passed.
Again, setting cookies with same name and host/directory parameters REPLACES old cookies with same parameters on local machines.