In most web applications there will be some restricted content – whether that is administrative functions such as managing comments or simply content that is a bit sensitive and needs someone to be signed up to see it. Obviously this kind of thing is key to the application so it is often built into the application in a very specific way.
Here’s why I think that’s a bad thing, and how to do it in a much more generic fashion…

Why is it bad?

It’s bad because it ties a common function to a single application. This means that if you write another application that needs a login system you need to start from scratch instead of being able to port an operational and secure system you’ve already made.
It’s also bad because changing it can often mean changing huge parts of the application. We don’t want that – we want modularity and simplicity so that one change doesn’t ripple through the system with catastrophic effect.

How do we do it differently?

In a simple way of describing it, you create yourself another little application! This might sound silly, but think of logging in as a function to be dealt with by an external system, with your application just having an interface with the system when you need it to. Once you’ve made your login application, you can just re-use it and create a new interface for the different application.

What do we need?

The beauty of this approach is that it is so simple!
All you need are the following pages:

  • A form.
  • A PHP page to work out if the supplied details are valid.
  • An interface to sit between the login system and the web application.

So let’s make them.

First we will do the easy bit – the form. I’m doing this first not only because it is the easiest bit but because knowing what fields we have in the form will influence the PHP page.
So, if we just want a standard username/password form we will end up with some HTML that looks like this. I am assuming you know how to do HTML so I’ll skip over this pretty quickly.

<form id="myform" action="login.php" method="post">
  <label for="uname">Username: </label>
<input id="uname" name="uname" size="25" type="text" />
 
  <label for="pword">Password:</label>
<input id="pword" name="pword" size="25" type="password" />
 
<input name="submit" type="submit" value="submit" />
</form>

Obviously sizes etc. are up to you, this is just an example…

Next up will be the login page itself. This will just use PHP to figure out if the supplied details are correct. Again, I assume that you know some PHP but I’ll break this up a bit.

The first thing to do is actually deal with logging out. There’s no harm in doing that in the same file that handles the login functionality, so stick it at the top and get it out of the way! The process is simple – unset all the variables that you have set to show you’re logged in. It’s pretty self explanatory and it’s heavily commented!

if(isset($_GET['logout']) &amp;&amp; !isset($_GET['logout'][0])
{
   /* Deal with logging out.
   * Here we will unset any variables we are using to signify we are logged in.
   * It is bad practice to use just one variable, so in this example a few are used but I would recommend you use as many as
   * you can and make them as varied as possible without compromising security (e.g. don't use the password!).
   * For the sake of simplicity, I'm just going to use Session variables but I would recommend you use others too, such as
   * cookies!
   */
 
   unset($_SESSION['example_logged_in']);
   unset($_SESSION['example_nickname']);
   // 'example' is used here for no particular reason, except to cause - but not rely upon - security through obscurity,
   // remember to unset ALL variables you use in your own implementation.
 
   // Finally, we just redirect to another page.  In most instances this will be either a page saying you've logged out, or
   // an index page.
   header("Location: another_page.php");
   exit();
}

Note: the rest of the code for this page is in the else block for this IF statement!

The next stage is to deal with logging in.
This is much more complex because there’s much more to do!

The first thing to do is to make sure your inputs are sanitized before you use them!

$user = mysql_real_escape_string(strip_tags($_POST['uname']));
$pass = md5(mysql_real_escape_string(strip_tags($_POST['pword'])));

Here we use a few functions to clean them up. mysql_real_escape_string requires a connection to a database to function (in the sample code, we’ve already dealt with that) and basically make any rogue SQL that could be used for SQL injection safe. strip_tags removes all HTML tags – you can customise it to allow some tags, but we won’t need that for a login system. md5 is a hash which encrypts the string – note how I use md5 after I’ve cleaned the variable so that I’m not hashing a dodgy input! We encrypt this string because the one stored in the database should be stored with md5 encryption.

The next stage is to run the query with our newly sanitized inputs and see what happens!

// Run the query on the database and store the result in a variable.
// The table name is registered_members and the field I am comparing the username supplied in the form to is username.
$result = mysql_query("SELECT password, nickname FROM registered_members WHERE username='".$user."'");
 
if(mysql_numrows($result)==1) // Check that only one row matched.
{
   $row = mysql_fetch_array($result); // Grab the row as an array into a variable.
   if($pass == $row['password']) // Check if the passwords match.
   {
      /* This is where we set all the variables that show the user is logged in.
      * Remember, these variables should match those we unset in the logout section near the top.
      */
   }
   else
   {
      // This block of code is hit when the password supplied by the user doesn't match that in the database.
      // The 2 options here are include the login page form, or redirect to it.  I'll choose redirect but it makes
      // no real difference and including it may allow more flexibiliity.
      header("Location:loginpage.php");
      exit();
   }
}
else
{
   // This block of code is hit when the username supplied by the user doesn't match any in the database.
   // The 2 options here are include the login page form, or redirect to it.  I'll choose redirect but it makes
   // no real difference and including it may allow more flexibiliity.
   header("Location:loginpage.php");
   exit();
}

For readability I’ve chopped some code out from that, but you get the idea. The full code is available in the sample code.
We have carried out a number of steps.

  • Run the query trying to retrieve the row with a username matching the one supplied by the user
  • If the result has 1 row, it matched correctly – username exists.
  • Compare the password stored in that row with the one supplied by the user – If it matches then it’s correct and you can carry on with setting variables to mark the session/computer as logged in.
  • If either the username or the password doesn’t match, show the login page again – you may wish to pass over an appropriate error message!

Hopefully that all seems pretty simple! It’s the backbone of the login system and is fairly simple and lightweight.

The last page that we need is the interface between application and login system. This will need certain constants to be in place with the login system, and also certain customised elements to fit in with the application.
It takes the form of an if/else statement to check that the variables the login system sets exist and contain the correct info. If the block returns true – the user is logged in – then the next bit is very much application specific and you can do whatever you like in there. If it returns false – the user isn’t logged in – then the steps followed are specific to the login system.

Here it is:

session_start();
 
/*
* This if statement MUST check all the variables that are set by the login system and also check that they
* are not only set but contain the right information.
*/
if(isset($_SESSION['example_logged_in']) &amp;&amp; $_SESSION['example_logged_in']=="user is logged in" &amp;&amp; !empty($_SESSION['example_nickname']))
{
   /*
   * The variables show the user is logged in.
   * Do whatever application specific checks are required.
   *  Further security enhancements could be made - a database check for example.
   */
}
else
{
   /*
   * The variables show the user is not logged in.
   * This section is specific to the login system.
   */
 
   // Redirect to the login page.
   header("loginpage.php");
   exit();
}

So that’s it – we’ve made a login system that is completely generic, as well as an easily modified interface to allow it to be plugged onto web applications with the minimum of effort.

Now what?
Well now that we have the files, all we have to do is use them! This bit is ridiculously simple. At the very top of any page you want to be restricted, just include the interface file (in the example code, this is logincheck.php). This means the first thing to happen is the check, and if that fails it redirects to the login system!

What else can we do to it?

The obvious thing to do is to add in a facility for the login system to know which page the user failed to access and once its finished logging in for it to go there. There are a few approaches to this; pass the url of the required page in the GET array (i.e. as a url parameter) or store it as a Session variable (or a cookie). My preference is towards the latter, because it makes it much neater to use and easier to implement.
It requires small changes to login.php and logincheck.php. login.php has to check to see if the variable exists and if so redirect to it once logging in is complete, and logincheck.php has to set the variable. That’s why it’s so easy!

The modifications to login.php:

if(!empty($_SESSION['requestedpage'])
{
   $_SESSION['requestedpage'] = "";
   header("Location: ".$_SESSION['requestedpage']);
}
else
{
   header("Location:index.php"); // As with logging out, you can redirect anywhere you like.
}

The modifications to logincheck.php:

$_SESSION['requestedpage'] = "http://".$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI'];

Easy!

The other modification to the sample code that I would highly recommend is to make the checks more robust – introduce new variables to be checked and maybe include a database check or something similar.

Conclusion

In a few short scripts we have dealt with creating a login system for any kind of application. It’s really easy to do and it’s really easy to fit onto the applications! The benefit of this approach is that everything on the login system side is separate from everything on the application side – neither side cares what the other side is doing, so you can completely change the login system for another one and everything will still work fine as long as you update the interface file!

At this point I must stress that this is a guide and doesn’t guarantee security. If you follow all of my suggestions (using some kind of encryption hash on passwords etc. such as md5()) and sanitize all of your inputs then you should get along fine but neither I nor progtuts.info take any responsibility for anything bad happening as a result of you not making the appropriate modifications to my code for your purposes.

I realise that much of that will have been a lot to take in, so take a while and read through the sample code which I have heavily commented for your benefit!

Hopefully this approach will help you. Remember, when programming it’s much easier to work with modules that can be easily swapped out rather than mashing the whole thing up together and then crying when it comes to maintenance or modification!

This is a pretty long tutorial so if you see something I’ve missed or think something could be phrased or explained better feel free to comment and I’ll make any changes.

Updates!

So this post has had a few very useful comments on it, so here are a few updates to add from them:

  • Pablo Mora pointed out that a die() or exit() call after each header() call would be needed. In principle this is correct but for this application the benefit is negligible because nothing is done after the header() call. For best practice though, you should include the calls.
  • Another Seasoned Coder suggested using the empty() function to check the variables rather than checking if they are set and not empty. This is a very useful alteration which cuts down on the number of evaluations and therefore improves efficiency.
  • Dwayne Charrington suggested adding “session_regenerate_id();” right after successfully logging in as a security improvement. Again this is a really useful tip!
  • Karsten and another seasoned coder both suggested looking at pre-existing packages/frameworks to make everything a bit simpler when designing sites - http://pear.php.net/package/Auth and CakePHP respectively.

I’ve updated the sample code to reflect these changes; there are a few minute differences between the sample code and the examples on this page but the ideas are still the same. Thanks for all the comments and keep them coming! For any discussion of security practices (or anything else that interests you!) why not head over to the Forum?

Lastly, I have to reiterate here that this tutorial is NOT intended to provide a secure solution. Infact, it doesn’t have much in the way of security in it at all. There’s a reason for this and it’s because it would be irresponsible for me to proclaim my script to be secure with a host of security measures when in reality I’m no security expert and don’t pretend to be! The aim of this tutorial is to provide an architecture or platform on which the reader can build upon and create bespoke security measures all of their own. Please don’t comment moaning about the lack of security. If you have any security tips you would like to discuss, join the forum and show them off to the world in there.

Download the Source Code

Related Posts

More?

If you enjoyed this post, please consider promoting it.
  • Digg
  • del.icio.us
  • StumbleUpon
  • Design Float
  • Reddit
  • Mixx
  • Technorati

Remeber to subscribe to the feed and get future articles delivered to your feed reader.

If you want to discuss this tutorial or any other thoughts you have then you can do so over on our fourm.

Comments

  1. Comment by Dees on August 1, 2008 10:17 pm

    Really great tutorial! I’ll be using this example as a model for my own projects from now on. I was planning on building something very similar but I’m still a beginner with PHP so I wasn’t ready to begin. Would you consider doing a guide on best practices and potential weaknesses for those of us who aren’t thoroughly aware of all the potential risks and hazards lurking around the web (i.e. botnets, SQL-injection, cross-server stuff, etc.)? I’d love to read more stuff at this level of quality.

    - Dees

  2. Comment by Pablo Mora on August 2, 2008 11:06 am

    You need to put a die() o exit() function below to header().

  3. Comment by jamie on August 3, 2008 7:45 am

    @dees:
    Thanks for the comments. As I’m not a security expert I don’t really want to post a ‘tutorial’ as such on security, but I do have some links to some interesting reads that may be beneficial - I’ll try to dig them out and post them.

    @Pablo Mora:
    In principle yes; in practice no. The die() or exit() functions are only really necessary if a chunk of PHP code further down the page will be executed - in this case none will because the header() calls are the last in the evaluation of each chunk of the if/else block, therefore will essentially be the last piece of code executed (because none of the other code matches the conditional). Also, because the header is sent from the server I don’t think any client side code would ever be executed. For absolute script efficiency though, I do agree that a die() or exit() would be needed.

    - Jamie

  4. Comment by another seasoned coder on August 4, 2008 8:51 am

    This is a nice solution, but I would like to recommend CakePHP. It is one of the best web frameworks I have ever had the pleasure to use. To provide extremely secure using hashes and flexible authentication (with the option to do authorization using ACL) is very easy and in some cases a single line of code (depends on what you need to do). One of the great things about CakePHP is it is written in PHP so it will run on any PHP 4 or 5 web host that allows .htaccess! I highly recommend checking it out, it has saved me more time than I can express and made coding websites fun again.

    P.S> There are several times that a var is checked to see if it is set and not null which can be done with the function empty such that:

    if(isset($_SESSION['requestedpage']) && $_SESSION['requestedpage']!=”")

    would be written

    if(empty($_SESSION['requestedpage']))

    As an example why this is beneficial you could then rewrite that whole block of code as:

    header(”Location: “.empty($_SESSION['requestedpage']) ? “index.php” : $_SESSION['requestedpage']);

  5. In these cases adding “session_regenerate_id();” right after successfully logging in is a good security practice to help prevent session id hijacking. Otherwise it all seems pretty solid.

  6. I found your article quite informative (and realized I’ve been doing this all along), but I noticed that you did your SQL to get the user and then tested with PHP to see if the passwords matched. However, the same result could be used by making your SQL statement:
    “SELECT password, nickname FROM registered_members WHERE username=’”.$user.”‘ AND password=’”.$pass.”‘”
    This would reduce the need for the extra if statement and also makes your code more maintainable, since you don’t need that extra else with duplicate code. And if you really want to increase speed, don’t use string concatenation (it’s SLOW).

  7. Comment by jamie on August 6, 2008 6:14 am

    @jerr - Splitting the username check and the password check up may seem like unnecessary work, but it actually has a purpose - the authentication can either fail on the username or the password, meaning the system can send back an error saying the username is incorrect or the password is incorrect. The sample code doesn’t do this because it is just a skeleton framework, but this approach means it is possible to do it. I agree about the String concatenation being slow, but it isn’t really used much and was the quickest way to throw an example together quickly - the app doesn’t use it much so there isn’t much of a performance hit.

    Thanks.

    Jamie

  8. A few pointers here. First, the MD5 system itself is pretty outdated, with so-called rainbow tables on the internets everywhere. That means that you can just paste the MD5 into a form field there, and you’ll get one or more passwords or common phrases matching that. Most passwords are in there, and as far as I know, so are most combinations of numbers and letters.

    One alternative is to use a salt - a randomly generated string of characters -, append it to the hashed password, and hash the entire thing - hashed password + random salt - again. Make sure to generate the salt only when the user first registers, and store it alongside his password.

    The advantage this has is that no user will have the same hash stored in the database - meaning that even if 5000 users all use ’secret’ as their password, you’ll have 5000 unique hashes in your database.

    Second, you could use a more advanced hashing algorithm. PHP comes with SHA1 by default, which results in a longer hash.

    I have the following two methods, one for generating a random salt, and one for hashing a password using SHA1 and the salt:

    /**
    * Returns a unique string, hashed with MD5 to give it a
    * fixed length.
    */
    function generateSalt()
    {
    return md5(uniqid(rand(), true));
    }

    /**
    * Encrypts the given $password, first SHA1-ing the password,
    * then prepending it with the given $salt, then re-hashing
    * the whole thing with SHA1.
    */
    function encrypt($password, $salt)
    {
    return (sha1($salt . sha1($password)));
    }

    Once again, do remember to store the salt alongside the user’s password, else the user can’t login.

    Logging in the user is then a matter of looking up the user and hashing the user-entered password using the encrypt function, passing it the user-entered password and the salt fetched from the database, then comparing it with the password stored in the database.

  9. Comment by red on August 18, 2008 3:35 am

    I have written the login system using php (sessions).
    once the user types the correct username and password i m dericting user to welcome page.
    when user clicks back button in welcome page then he/she should not go back to login page how could i prevent him ……

    help me getting rid of this problem…

  10. @red Although the users can press the back button and go back to the login page they will still be logged in as long they have an active session. They only go back to the login page because it’s in the history.

    I’m afraid you can’t really prevent anyone pressing the back button on their browser but your site will still work. They shouldn’t have to log in again to access the restricted area.

  11. Comment by jamie on August 19, 2008 8:00 am

    @red:
    I suppose you kind of could do this but it would require a few more session variables and a few more checks.
    The requirements for it are:

    in logincheck.php, where you set $_SESSION['requestedpage'], also make another session variable called something like ‘previouspage’ and set it to the referrer (available from the SERVER global array).

    in loginpage.php, have a check at the top to see if the ‘previouspage’ session variable is set. You will also have to duplicate some code from logincheck.php to check that the user is logged in. From the example, the bit you need to duplicate is isset($_SESSION['example_logged_in']) && $_SESSION['example_logged_in']==”user is logged in” && !empty($_SESSION['example_nickname']).
    If both of these pieces of tests return the required result, then redirect to the content of the previouspage session variable.

    in login.php when logging out you will need to unset the ‘previouspage’ session variable as well.

    It does work (I’ve just implemented it) but needs a little more maintenance and has more opportunity to go wrong.

    I’ve not included any code for doing it so that you can have your own little challenge - if you find you reeeally can’t do it, head over to the forum and I’ll help out.

    J

  12. @Jamie

    You stated:
    …meaning the system can send back an error saying the username is incorrect or the password is incorrect…

    This is bad practice. You should be as generic as possible with your error messages related to an invalid username or password. If I were to try and hack your login, and I got the error that the password was incorrect, I know that I found a valid username in your database.

    Just thought I would throw that out there.

  13. Looking at the line:
    $pass = md5(mysql_real_escape_string(strip_tags($_POST['pword'])));

    What’s the point of escaping the string if its going to be md5′d anyway?

  14. Comment by jamie on September 10, 2008 3:24 pm

    @eric:
    Why not? No real harm in it unless the user has decided to use some SQL and HTML in their password.
    Anyway, as mentioned in the tutorial itself, I don’t aim to provide security measures in this because to do so would be irresponsible of me since I’m no security expert…infact I’ve already said I won’t give any security advice! This tutorial is simply to provide the structure; it’s up to the reader to extend it to a useable platform.

  15. Comment by Enc on April 3, 2009 10:17 pm

    Oh no, another useless tutorial. How exactly is this implementation generic enough to be reused? How is it encapsulated and decoupled?
    You just showed what people usually do when they create a login system - which is great: someone may be following your tutorial to create their first auth system.
    However, if you claim that everyone else is coding this for every project, then provide a class/module that we can easily reuse, not some PHP code that any freshman can write. Either I take your code, or my own code from other projects, it makes no difference - I have to spend the same amount of time to integrate it in the new project.
    So, please don’t claim that you’re solving the world’s problems, when you’re in fact just demonstrating how to implement a basic login system.
    BIG DEAL.

  16. @Enc
    I love comments like this; it just shows you’ve totally missed the point of the tutorial and didn’t bother to read all that much of it.

    I’ll start with how you don’t think the implementation is generic. Well, it’s not but then it’s not intended to be; the approach is generic because once you’ve added the session variables and checks etc. that you want, you can easily use it in loads of applications by adding what…one line of code to each restricted file? It’s not my job to do your work for you - my tutorials are intended to provide ideas and approaches rather than complete solutions. I have a complete version of this; I used it generically for a number of small projects last year around the time this tutorial was written so if you can’t use the approach generically then that’s your problem.

    Next up is the whole ‘use a class’ thing. Yes, classes are excellent for structure and design etc. but when it’s a small application written procedurally then by my count you require a minimum of 2 lines of code per file compared to my 1 line of code, as well as having to construct a full class to do anything you want. I’ve not run any benchmarks but I’d bet there’s no benefits to be had.
    Also, many beginners don’t know how to use classes and don’t understand how to mix them with the procedural stuff that beginners almost always start off with. How does the tutorial help them?

    If you want a fully integrated login system written for you, pay me. I don’t make any money or get any benefits from doing this. My aim is to help people think about the problem and think about a solution to the problem by giving them an overview and general structure.

    Finally, nowhere did I claim that I was solving the world’s problems, you clearly think you’re above the rest of us so why don’t you spend some of your time to write a bunch of tutorials about a variety of programming languages and problems? It never fails to amaze me that people like you expect everything done for you - if you don’t need the tutorial then good for you…why not provide some improvements rather than mindless criticism?

Leave a Comment

(required)

(required)