UNIX Systems Administrator (IL)
Next Step Systems
US-IL-Chicago

Justtechjobs.com Post A Job | Post A Resume

A Complete, Secure User Login System
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

    
setcookie('user_name',$user_name,(time()+2592000),'/','',0);
    
setcookie('id_hash',$id_hash,(time()+2592000),'/','',0);
}

?>
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');
}

?>
Those are the important parts of the library. I've uploaded the entire library here as a Pretty Source File and the plain text in the Source Code Snippet Library.
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.
--Tim