When I started seeing spam messages posted to the new column annotation system, I knew
I would have to create some sort of user authentication system that helps weed out the losers.
I'm the type that would rather write an entire library myself than try to learn something like PHPLib
or other similar libraries.
The library needed to handle registration, confirmation emails, account updates (passwords, emails)
among other things. It also needed to be secure while not creating a burden on my overloaded database.
So the new system needed to rely on cookies while not being totally exploitable. It was an interesting
dilemma. I knew I couldn't simply set a user_name cookie when they logged in (the user name cookie is
easy to spoof). I also knew I didn't want to set a simple hash and have to confirm that hash against my
database.
The solution was to set both. A user_name cookie is set, along with a hash. The hash is an md5() hash
of the user_name as well as a super-secret variable that only PHPBuilder knows. Since md5() is a one-way
hash and is, for all intents and purposes, going to secure practically any website, but should not
be taken to be "uncrackable"*. I could safely create a hash of the email, which is
a known variable, plus the secret variable. It's kind of a public-key/private-key kind of system.
The interesting thing about this system is that it could scale up almost infinitely. Since the hard work
of this system is done by md5() on the web server, additional servers can be dropped in incrementally to
handle the load. The same is not true of an auth system that hammers a database - the database itself
eventually becomes the bottleneck.
* This is a correction requested by the author. Please see comments below for clarification.
Here are the two critical functions in this library - the token creation and token verification functions.
Don't worry - the rest of the library is included here as well.
<?php
$hidden_hash_var='your_secret_password_here';
$LOGGED_IN=false; unset($LOGGED_IN);
function user_isloggedin() { global $user_name,$id_hash,$hidden_hash_var,$LOGGED_IN; //have we already run the hash checks? //If so, return the pre-set, trusted var if ( isset($LOGGED_IN) ) { return $LOGGED_IN; } //are both cookies present? if ($user_name && $id_hash) { /* Create a hash of the user name that was passed in from the cookie as well as the trusted hidden variable
If this hash matches the cookie hash, then all cookie vars must be correct and thus trustable */ $hash=md5($user_name.$hidden_hash_var); if ($hash == $id_hash) { //hashes match - set a global var so we can //call this function repeatedly without //redoing the md5()'s $LOGGED_IN=true; return true; } else { //hash didn't match - must be a hack attempt? $LOGGED_IN=false; return false; } } else { $LOGGED_IN=false; return false; } }
function user_set_tokens($user_name_in) { /* call this once you have confirmed user name and password are correct in the database */ global $hidden_hash_var,$user_name,$id_hash; if (!$user_name_in) { $feedback .= ' ERROR - User Name Missing When Setting Tokens '; return false; } $user_name=strtolower($user_name_in);
//create a hash of the two variables we know $id_hash= md5($user_name.$hidden_hash_var);
//set cookies for one month - set to any amount //or use 0 for a session cookie
Make sense? Now onto another interesting chunk of code. How do we let users change their email address
in a secure way? They need to be able to change email addresses over the ages, but if they do, I want
the user to re-confirm that new address so I know it's legitimate. I want an email address in case they
abuse the system - I can take it back to their sysadmin.
<?php
function user_change_email ($password1,$new_email,$user_name) { global $feedback,$hidden_hash_var; if (validate_email($new_email)) { $hash=md5($new_email.$hidden_hash_var); //change the confirm hash in the db but not the email - //send out a new confirm email with a new hash $user_name=strtolower($user_name); $password1=strtolower($password1); $sql="UPDATE user SET confirm_hash='$hash' WHERE user_name='$user_name' AND password='". md5($password1) ."'"; $result=db_query($sql); if (!$result || db_affected_rows($result) < 1) { $feedback .= ' ERROR - Incorrect User Name Or Password '; return false; } else { $feedback .= ' Confirmation Sent '; user_send_confirm_email($new_email,$hash); return true; } } else { $feedback .= ' New Email Address Appears Invalid '; return false; } }
function user_confirm($hash,$email) { /* Call this function on the user confirmation page, which they arrive at when the click the link in the account confirmation email */ global $feedback,$hidden_hash_var;
//verify that they didn't tamper with the email address $new_hash=md5($email.$hidden_hash_var); if ($new_hash && ($new_hash==$hash)) { //find this record in the db $sql="SELECT * FROM user WHERE confirm_hash='$hash'"; $result=db_query($sql); if (!$result || db_numrows($result) < 1) { $feedback .= ' ERROR - Hash Not Found '; return false; } else { //confirm the email and set account to active $feedback .= ' User Account Updated - You Are Now Logged In '; user_set_tokens(db_result($result,0,'user_name')); $sql="UPDATE user SET email='$email',is_confirmed='1' WHERE confirm_hash='$hash'"; $result=db_query($sql); return true; } } else { $feedback .= ' HASH INVALID - UPDATE FAILED '; return false; } }
function user_send_confirm_email($email,$hash) { /* Used in the initial registration function as well as the change email address function */ $message = "Thank You For Registering at Company.com". "\nSimply follow this link to confirm your registration: ". "\n\nhttp://www.company.com/account/confirm.php?hash=$hash&email=". urlencode($email). "\n\nOnce you confirm, you can use the services on PHPBuilder."; mail ($email,'Registration Confirmation',$message,'From: noreply@company.com'); }
The complete set of code, including the HTML login pages are included in a [zip file here].
To use the system, just download the zip file above and place user.php,database.php, and pre.php into a directory
called /include/ at the root of your web server. Set .php to be parsed by PHP3. Set your database's username, password
and hostname in the database.php file. Create a table called user using the SQL at the top of user.php.
The general order of events that a user must follow is straightforward and standard on the web these
days: register, confirm, login, logout.
Let me know if you have any problems or add anything to the library.