PHP Login Script For Beginners

Programming a php login script can be a real pain. It has to be well coded, unique and, most of all, secure. We will pass all the mentioned aspects of a PHP login system in this tutorial by actually building one. Today I’m going to teach you how to write a good login script with user levels (admin, members, managers etc.), with forgot password included, user activation upon registering and just about everything that is supposed to be included in a good login system. Just like we did on the search engine tutorial some while ago, we are going to code our project with multiple examples, upgrades of code and also by comparing a bad usage with a good one.

Of course there are many ways of doing this. I can just throw in here some code and say “copy+paste this – name it login.php…copy+paste that …etc” but I prefer to know that you’re actually learning how to create your own by following good practices.

I received a lot of emails for that tutorial from people informing me of an error or two at some line where I did wrong I guess and I was happy to see that they are actually trying and tweaking it to get the damn thing working and not just downloading some code, step by stepping it and ta-da, I’m a new programmer. This tutorial will be built by the same rules since I’m actually trying to teach you guys something.

To start with this we will need a table in our database which will hold the user’s data:

  1. CREATE TABLE `users` (
  2.   `ID` int(11) NOT NULL auto_increment,
  3.   `Username` varchar(255) NOT NULL,
  4.   `Passwordvarchar(255) NOT NULL,
  5.   `Temp_pass` varchar(55) default NULL,
  6.   `Temp_pass_active` tinyint(1) NOT NULL default ‘0’,
  7.   `Email` varchar(255) NOT NULL,
  8.   `Active` int(11) NOT NULL default ‘0’,
  9.   `Level_access` int(11) NOT NULL default ‘2’,
  10.   `Random_key` varchar(32) default NULL,
  11.   PRIMARY KEY  (`ID`),
  12.   UNIQUE KEY `Username` (`Username`),
  13.   UNIQUE KEY `Email` (`Email`)
  14. ) ENGINE=MyISAM;

As you can see from the above schema, I complicated things a little bit by adding some extra fields: level access, temporarily password, temp. password active and the random key. With the help of this structure and the stored data we are going to be able to create a complex php login script.

I’ve seen a lot of bad code in the registration page of other websites and especially on pages that have forms and process inputs. In my opinion, the most dangerous parts of websites, that need extra security and extra attention are pages with SQL statements that rely on data provided via $_GET, $_REQUEST, $_POST which can very well be traduced to forms and dynamic URLs.

I will take a good BAD example of not processing forms and I hope it will give you a good idea of the way you’re supposed to run things. Let’s stay on the same page and talk about the user registration form. I can have the following HTML.

  1. <form action=“register.php” method=“post”>
  2.     <input type=“text” id=“username” name=“username” size=“32” value=“” />
  3.     <input type=“password” id=“password” name=“password” size=“32” value=“” />
  4.     <input type=“text” id=“email” name=“email” size=“32” value=“” />
  5.     <input type=“hidden” id=“active” name=“active” size=“32” value=“0” />
  6.     <input type=“hidden” id=“level_access” name=“level_access” size=“32” value=“2” />
  7.     <input type=“submit” name=“register” value=“register” />
  8. </form>

I’m sure that many of you have already seen the problem. A lot of people actually use this method which is very bad. As you can see in the above MySql schema, for the field `Active` I have the default value to 0. Mysql will add that to every new record UNLESS you specify something else. Not everyone builds sql tables the same way. A lot of websites still work with this stupid criteria. If it’s input hidden than it’s hidden and nobody will see it. Wrong, someone could download your page, save it to a html file, change the Active from 0 to 1, create a nice loop and there you have it, you could wake up tomorrow with 1000 new members :).

My first point for this tutorial: extra safety when building forms. Show only what is needed. I can set the default value to 0 in MySql and I can safely delete that hidden input which is supposed to store the value. The same thing is valid with the `level_access` field which is even more dangerous. You might share your admin with unwanted members.

With all that being said, let’s compose our registration page:

  1. <?php
  2. require_once(‘db.php’);
  3. include(‘functions.php’);
  4.     if(isset($_POST[‘register’]))
  5.     {
  6.         if($_POST[‘username’]!= && $_POST[‘password’]!= && $_POST[‘password’]==$_POST[‘password_confirmed’] && $_POST[’email’]!= && valid_email($_POST[’email’])==TRUE && checkUnique(‘Username’$_POST[‘username’])==TRUE && checkUnique(‘Email’$_POST[’email’])==TRUE)
  7.         {
  8.             $query = mysql_query(“INSERT INTO users (`Username` , `Password`, `Email`, `Random_key`) VALUES (‘”.mysql_real_escape_string($_POST[‘username‘]).”‘‘”.mysql_real_escape_string(md5($_POST[‘password‘])).”‘‘”.mysql_real_escape_string($_POST[‘email‘]).”‘‘”.random_string(‘alnum‘, 32).”‘)”) or die(mysql_error());
  9.             $getUser = mysql_query(“SELECT ID, Username, Email, Random_key FROM users WHERE Username = ‘”.mysql_real_escape_string($_POST[‘username‘]).”‘“) or die(mysql_error());
  10.             if(mysql_num_rows($getUser)==1)
  11.             {//there’s only one MATRIX :PP
  12.                 $row = mysql_fetch_assoc($getUser);
  13.                 $headers =  ‘From: [email protected]‘ . “\r\n” .
  14.                             ‘Reply-To: [email protected]‘ . “\r\n” .
  15.                             ‘X-Mailer: PHP/‘ . phpversion();
  16.                 $subject = “Activation email from ourdomainhere.com”;
  17.                 $message = “Dear “.$row[‘Username‘].”, this is your activation link to join our website. In order to confirm your membership please click on the following link: http://www.ourdomainhere.com/confirm.php?ID=”.$row[‘ID‘].”&amp;key=”.$row[‘Random_key‘].” Thank you for joining”;
  18.                 if(mail($row[‘Email‘], $subject, $message, $headers))
  19.                 {//we show the good guy only in one case and the bad one for the rest.
  20.                     $msg = ‘Account created. Please login to the email you provided during registration and confirm your membership.‘;
  21.                 }
  22.                 else {
  23.                     $error = ‘I created the account but failed sending the validation email out. Please inform my boss about this cancer of mine‘;
  24.                 }
  25.             }
  26.             else {
  27.                 $error = ‘You just made possible the old guy (the impossible). Please inform my boss in order to give you the price for this.‘;
  28.             }
  29.                             
  30.         }
  31.         else {      
  32.             $error = ‘There was an error in your data. Please make sure you filled in all the required data, you provided a valid email address and that the password fields match‘;    
  33.         }
  34.     }
  35. ?>
  36. <?php if(isset($error)){ echo $error;}?>
  37. <?php if(isset($msg)){ echo $msg;} else {//if we have a mesage we don’t need this form again.?>
  38. <form action=“<?=$_SERVER[‘PHP_SELF’]?>” method=“post”>
  39.     Username: <input type=“text” id=“username” name=“username” size=“32” value=“<?php if(isset($_POST[‘username’])){echo $_POST[‘username’];}?>” /><br />
  40.     Password: <input type=“password” id=“password” name=“password” size=“32” value=“” /><br />
  41.     Re-password: <input type=“password” id=“password_confirmed” name=“password_confirmed” size=“32” value=“” /><br />
  42.     Email: <input type=“text” id=“email” name=“email” size=“32” value=“<?php if(isset($_POST[’email’])){echo $_POST[’email’];}?>” /><br />
  43.     <input type=“submit” name=“register” value=“register” /><br />
  44. </form>
  45. <? } ?>

Another important aspect when building forms is the fact that people hate them. If they complete something wrong and the server comes back with an error and a new form (or the previous one but empty) they will most probably leave so this is why we have the $_POST values there, the username and email field will remain completed. The errors will be shown using $error which is visible only if is set and can be set only if something’s wrong (vicious circle huh :)) and the confirmation message will be visible (this will also make the form invisible) only in one case, one successful case. You will notice 3 external functions that I use (for more functions and code snippets you can save, post and bookmark on the snippets section): valid_email, random_string and checkUnique.

Let’s take the above code step by step and try to understand it. In the first part of it we have the php code which will process our form and decide what’s next. We confirm or we throw out an error. Notice that it begins with an if statement (and I encourage you to use it in your applications to make sure all conditions are met before running something) that needs to be satisfied before we enter the details into the database. There’s an unlimited number of if’s that can be set, I just used it to make sure all fields are entered, the two passwords match each other, the username is not taken and that the email address is valid and also unique as well. If one of the above conditions is not satisfied we have an else statement to define $error and output the message. Next, we passed the first “if” and it means that everything was ok until here so we create a query to enter the details into our table “users”. Make sure you escape all variables used in a query. Also notice that I used md5 to encrypt the password before storing it into our database.

It’s time to send out the confirmation email in order for our new user to verify the entered email address and activate his membership. For this step we created the `Random_key` field in our table. We need a random key generated at the time we register a new user, he won’t be able to know that key unless he opens the confirmation email. We could use the following activation schema via $_GET: confirm.php?ID=123 where ID is the unique record number of the user but that would be easy to guess. A robot could create usernames and activate them by incrementing the ID. In our example, only if the ID and the Random_key is matched we update the `Active` field and set it to 1. we’re not there yet but I needed to explain everything that takes part in our code.

After inserting the new member we write an extract query to get the exact data that was stored and create the activation link. We have another if statement for the email, if is sent $msg will be defined and the form will become invisible – the confirmation message is displayed. If not, there’s an else statement again to inform our visitor that something went wrong. As you can see, in my opinion, it’s good to communicate with the visitor at every step, if something’s wrong or not, show a message. Almost everything starts with an if and has an else or elseif.

I think we’re done with the register page, let’s move to that confirm.php which will have to take the data, check it against the database and, based on what is stored there, decide what to do. Just like in the register page, we will start with an if statement and only if all the conditions are satisfied, we confirm that membership.

  1. <?php
  2. require_once(‘db.php’);
  3. include(‘functions.php’);
  4.     if($_GET[‘ID’]!= && numeric($_GET[‘ID’])==TRUE && strlen($_GET[‘key’])==32 && alpha_numeric($_GET[‘key’])==TRUE)
  5.     {
  6.         $query = mysql_query(“SELECT ID, Random_key, Active FROM users WHERE ID = ‘”.mysql_real_escape_string($_GET[‘ID‘]).”‘“);
  7.         if(mysql_num_rows($query)==1)
  8.         {
  9.             $row = mysql_fetch_assoc($query);
  10.             if($row[‘Active’]==1)
  11.             {
  12.                 $error = ‘This member is already active !’;
  13.             }
  14.             elseif($row[‘Random_key’]!=$_GET[‘key’])
  15.             {
  16.                 $error = ‘The confirmation key that was generated for this member does not match with the one entered !’;
  17.             }
  18.             else
  19.             {
  20.                 $update = mysql_query(“UPDATE users SET Active=1 WHERE ID='”.mysql_real_escape_string($row[‘ID‘]).”‘“) or die(mysql_error());
  21.                 $msg = ‘Congratulations !  You just confirmed your membership !’;
  22.             }
  23.         }
  24.         else {
  25.             $error = ‘User not found !’;
  26.         }
  27.     }
  28.     else {
  29.         $error = ‘Invalid data provided !’;
  30.     }
  31.     if(isset($error))
  32.     {
  33.         echo $error;
  34.     }
  35.     else {
  36.         echo $msg;
  37.     }
  38. ?>

Ok. Let’s split this code also. AGAIN, we filter the data and only if (yes another if statement) both the ID and random key match our criteria, we may continue; else, set a value for $error.
I used 2 functions from the snippets section to make sure that the incoming ($_GET[‘ID’]) ID has a numeric value and that our key has 32 characters (strlen($_GET[‘key’])) and is also alpha-numeric.

Here’s how the information comes in via $_GET from the confirmation link: confirm.php?ID=123&key=4ytu683hbmh849f9g7hjjym78eog648. We create a SELECT query based on the ID that was provided. If the user is found (mysql_num_rows($query)==1) we continue (else: set another value for $error) with another if: if the user is already activated, set another value for $error (This member is already active !), else if the incoming key ($_GET[‘key’]) does not match the one from our database ($row[‘Random_key’]!=$_GET[‘key’]) set another value for $error (The confirmation key that was generated for this member does not match with the one entered !). After this big filter, when everything is as supposed to be, do the update query and set the value to 1 which, in our case, is equal with an active member.

Our next task is to create the login page which will have to check the entered username and password, see what level access the user has and decide if he’s allowed to view the requested page.

  1. <?php
  2. require_once(‘db.php’);
  3. include(‘functions.php’);
  4.     if(isset($_POST[‘Login’]))
  5.     {
  6.         if($_POST[‘username’]!= && $_POST[‘password’]!=)
  7.         {
  8.             //Use the input username and password and check against ‘users’ table
  9.             $query = mysql_query(‘SELECT ID, Username, Active FROM users WHERE Username = “‘.mysql_real_escape_string($_POST[‘username’]).‘” AND Password = “‘.mysql_real_escape_string(md5($_POST[‘password’])).‘”‘);
  10.             if(mysql_num_rows($query) == 1)
  11.             {
  12.                 $row = mysql_fetch_assoc($query);
  13.                 if($row[‘Active’] == 1)
  14.                 {
  15.                     $_SESSION[‘user_id’] = $row[‘ID’];
  16.                     $_SESSION[‘logged_in’] = TRUE;
  17.                     header(“Location: members.php”);
  18.                 }
  19.                 else {
  20.                     $error = ‘Your membership was not activated. Please open the email that we sent and click on the activation link’;
  21.                 }
  22.             }
  23.             else {
  24.                 $error = ‘Login failed !’;
  25.             }
  26.         }
  27.         else {
  28.             $error = ‘Please user both your username and password to access your account’;
  29.         }
  30.     }
  31. ?>
  32. <?php if(isset($error)){ echo $error;}?>
  33. <form action=“<?=$_SERVER[‘PHP_SELF’]?>” method=“post”>
  34.     <input type=“text” id=“username” name=“username” size=“32” value=“” />
  35.     <input type=“password” id=“password” name=“password” size=“32” value=“” />
  36.     <input type=“submit” name=“Login” value=“Login” />
  37. </form>

The above code does a simple task. After submitting the form it again checks if the values are not empty (else defines $error) and continues to the sql select statement. If the number of records that return, based on the provided username and password, is equal to 1 (it has to but we just make sure) we move forward and check to see if the user has activated his membership ($row[‘Active’] == 1). If it’s active, we set $_SESSION[‘user_id’] with the ID record of our member that has just successfully logged in (members.php will be the default redirect on success page). As always we have else statements to define $error and communicate with our visitor in case something goes wrong.

Our next step is to create a forgot password page. The plan is to generate a temporary password for the requested username and make it active after the request but NOT override the actual password. If the confirmation (of password reset) is received and the link clicked, we update the users table and set the temp. password active field to 0 since it’s confirmed. You can only reset it once per request. A new request is needed for a new change. This sounds a little complicated but if you will play with this password reset page I’m sure it will be more than self-explanatory so let’s start coding this little page.

  1. <?php
  2. require_once(‘db.php’);
  3. include(‘functions.php’);
  4.     if(isset($_POST[‘Submit’]))
  5.     {
  6.         if($_POST[’email’]!= && valid_email($_POST[’email’])==TRUE)
  7.         {
  8.             $getUser = mysql_query(‘SELECT ID, Username, Temp_pass, Email FROM users WHERE Email = “‘.mysql_real_escape_string($_POST[’email’]).‘”‘);
  9.             if(mysql_num_rows($getUser)==1)
  10.             {
  11.                 $temp_pass = random_string(‘alnum’, 12);
  12.                 $row = mysql_fetch_assoc($getUser);
  13.                 $query = mysql_query(“UPDATE users SET Temp_pass='”.$temp_pass.“‘, Temp_pass_active=1 WHERE `Email`='”.mysql_real_escape_string($row[‘Email‘]).”‘“);
  14.                 $headers =  ‘From: [email protected] . “\r\n” .
  15.                             ‘Reply-To: [email protected] . “\r\n” .
  16.                             ‘X-Mailer: PHP/’ . phpversion();
  17.                 $subject = “Password Reset Request”;
  18.                 $message = “Dear “.$row[‘Username’].“, Someone (presumably you), has requested a password reset. We have generated a new password for you to access our website:  $temp_pass . To confirm this change and activate your new password please follow this link to our website: http://www.ourdomainhere.com/confirm_password.php?ID=”.$row[‘ID’].“&new=”.$temp_pass.“. Don’t forget to update your profile as well after confirming this change and create a new password. If you did not initiate this request, simply disregard this email, and we’re sorry for bothering you.”;
  19.                 if(mail($row[‘Email’], $subject$message$headers))
  20.                 {
  21.                     $msg = ‘Password reset request sent. Please check your email for instructions.’;
  22.                 }
  23.                 else {
  24.                     $error = ‘Failed sending email’;
  25.                 }
  26.             }
  27.             else {
  28.                 $error = ‘There is no member to match your email.’;
  29.             }
  30.         }
  31.         else {
  32.             $error = ‘Invalid email !’;
  33.         }
  34.     }
  35. ?>
  36. <?php if(isset($error)){ echo $error;}?>
  37. <?php if(isset($msg)){ echo $msg;} else {//if we have a mesage we don’t need this form again.?>
  38. <form action=“<?=$_SERVER[‘PHP_SELF’]?>” method=“post”>
  39.     <input type=“text” id=“email” name=“email” size=“32” value=“” />
  40.     <input type=“submit” name=“Submit” value=“Submit” />
  41. </form>
  42. <? } ?>

Inspecting the above code you will see that we need to create confirm_password.php in order to verify the new password and update the users info if confirmed. It has to see if there is an active temporarily password for that user and if the $_GET info matches the data stored in our database.

  1. <?php
  2. require_once(‘db.php’);
  3. include(‘functions.php’);
  4.     $query = mysql_query(“SELECT * FROM users WHERE ID = ‘”.mysql_real_escape_string($_GET[‘ID‘]).”‘“);
  5.     if(mysql_num_rows($query)==1)
  6.     {
  7.         $row = mysql_fetch_assoc($query);
  8.         if($row[‘Temp_pass’]==$_GET[‘new’] && $row[‘Temp_pass_active’]==1)
  9.         {
  10.             $update = mysql_query(“UPDATE users SET Password = ‘”.md5(mysql_real_escape_string($_GET[‘new‘])).”‘, Temp_pass_active=0 WHERE ID = ‘”.mysql_real_escape_string($_GET[‘ID‘]).”‘“);
  11.             $msg = ‘Your new password has been confirmed. You may login using it.’;
  12.         }
  13.         else
  14.         {
  15.             $error = ‘The new password is already confirmed or is incorrect’;
  16.         }
  17.     }
  18.     else {
  19.         $error = ‘You are trying to confirm a new password for an unexisting member’;
  20.     }
  21.     if(isset($error))
  22.     {
  23.         echo $error;
  24.     }
  25.     else {
  26.         echo $msg;
  27.     }
  28. ?>

We’re almost finished. We need to create one more function which will check the level access. Maybe you want to restrict page access based on user levels.

  1. <?php
  2.     function checkLogin($levels)
  3.     {
  4.         if(!$_SESSION[‘logged_in’])
  5.         {
  6.             $access = FALSE;
  7.         }
  8.         else {
  9.             $kt = split(‘ ‘$levels);
  10.             $query = mysql_query(‘SELECT Level_access FROM users WHERE ID = “‘.mysql_real_escape_string($_SESSION[‘user_id’]).‘”‘);
  11.             $row = mysql_fetch_assoc($query);
  12.             $access = FALSE;
  13.             while(list($key,$val)=each($kt))
  14.             {
  15.                 if($val==$row[‘Level_access’])
  16.                 {//if the user level matches one of the allowed levels
  17.                     $access = TRUE;
  18.                 }
  19.             }
  20.         }
  21.         if($access==FALSE)
  22.         {
  23.             header(“Location: login.php”);
  24.         }
  25.         else {
  26.         //do nothing: continue
  27.         }
  28.     }
  29. ?>


The above function takes the allowed user levels for that page, and checks them against what’s in the database for the authenticated user. If there’s a match, we grant access, else, redirect to login.
Presuming a restricted page called “members.php” where are allowed both the admin and a regular member we would use the function like this at the very beginning of the page:

  1. <?php
  2.     session_start();
  3.     checkLogin(‘1 2’);
  4. ?>


and for an admin:

  1. <?php
  2.     session_start();
  3.     checkLogin(‘1’);//2, 3, 4 …will be restricted
  4. ?>

We don’t store information like user email, password, username etc. on the session. We will just use the ID and based on $_SESSION[‘user_id’] we’ll perform SQL queries to request the data when is needed. The session can also be md5 encrypted for better security.

Click here to download the full working version of this PHP login script. If there is anything else you need to ask please do it on the forum and don’t send emails via the contact form.
UPDATE:

Here’s version 1.1 for this login script which has admin features (activate, delete, suspend users) and more mods. Have fun!