Creating a Secure Login System the Right Way
Making a custom login system is a common task for beginning PHP developers. Jumping right into it, however, may not be the best approach. There are several important aspects do building a login system that not only makes it work, but makes it safe.
Updated on December 15th 2009: Added Session Control Section
Getting Started
To begin with, we’ll create our login form. This doesn’t need to be anything fancy, just a couple of input fields and a submit button:
<form name="login" action="login.php" method="post"> Username: <input type="text" name="username" /> Password: <input type="password" name="password" /> <input type="submit" value="Login" /> </form>
The above example is stripped down; there is no formatting or styles so it will most likely won’t look to great if you copy and paste the code. Making your form pretty is beyond the scope of this article. In the form tag, notice that it has three attributes: name, action, and method. Name identifies the form and is not very important in the context of this article. Action identifies the script that will be processing the login, often times your form and the processing code are in a single file, but this does not have to be the case. Method typically takes one of two values: post or get. If you submit a form using GET the data is URL encoded and will be visible in the address bar. If the method is post the data will not be URL encoded. As you may have guessed there is actually a lot more to it than that but the difference between post and get is a topic for another article. We want to use post.
Notice that our input fields are given name attributes, this is important as we will need to identify and access these values by this name. PHP identifies form data using the name attribute, not the id. Now that our form is complete we can move on to processing the data.
Storing our Data
Actually we can’t process the data just yet. Before that we need to worry about how our data will be represented in the database. There are three essential values we must store in our database: the username, password, and a salt. We will get into what a salt is later. In addition to this you may choose to store other things about your user. It is also common to give the user a numeric user id. This is not absolutely necessary but it is common for tables to have a numeric, sequential primary key. For our purposes assume our table structure is this:
CREATE TABLE users ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(40) NOT NULL, salt VARCHAR(3) NOT NULL, PRIMARY KEY(id) );
In the above SQL code we create a table called users having columns id, username, password, and salt. Even though we could use usernames to uniquely identify users we will use the id for this instead. One reason for this is that integer comparisons are cheaper than string comparisons, so searching through a large number of users will require fewer resources. Convention is another reason.
There are a few keywords in the above code that I’ll define for you. Not null means that each tuple (a tuple is a row in our table, each user will have a row) must have a value for this column. In this case all of our columns are ‘not null’ so every user must have each of these values. Auto increment applies to numeric primary keys. It allows us to give each user a sequential id without worrying about collisions or what the most recent users id was; the database will assign each user a correct id automatically. Unique, as you may guess, means that the value must be unique. In our case, no two users may have the same username. Finally, primary key tells the database which field will uniquely identify each row. No two users can have the same id. It also creates and index on the specified column, meaning that lookups will be faster (at the expense of memory).
Each column also has a data type. The id is and integer while the remain fields are varchars. Varchars are just arrays of characters, as are strings. Varchar(30) means a sequence of 30 characters. For the username I chose 30 as the length for no particular reason; it allows for a reasonable amount of characters without letting users write a paragraph. The lengths for password and salt are important and I’ll get into that later. Now that we have created our table we are ready to process our data.
Populating our Table
Actually we can’t process the data just yet. You can’t login if your users table is empty. Just like on any site, you have to register before you can login. To accomplish this we will create a simple registration form.
<form name="register" action="register.php" method="post"> Username: <input type="text" name="username" maxlength="30" /> Password: <input type="password" name="pass1" /> Password Again: <input type="password" name="pass2" /> <input type="submit" value="Register" /> </form>
The above code is similar to our login form code with one notable difference. In the username input field I specify the maxlength attribute as 30. This means the field can only contain 30 characters and corresponds to the username length we specified in our SQL code. Notice I don’t enforce the length of passwords even though they are defined as 40 characters in the SQL code, you will see why it is not necessary to do so later. Now that we have our registration form we can process our data.
Sign Me Up
Our registration data that is, we still can’t process our login data. At this point we actually get to write some PHP code (are you as excited as I am?).
register.php (part 1):
<?php
//retrieve our data from POST
$username = $_POST['username'];
$pass1 = $_POST['pass1'];
$pass2 = $_POST['pass2'];
if($pass1 != $pass2)
header('Location: register_form.php');
if(strlen($username) > 30)
header('Location: register_form.php');
In the above code we retrieve our data from $_POST, which is an associative array where all of the post data is stored. We also check if pass1 and pass2 are equal which is an example of validating user input. If they are not equal we use the header function to redirect back to our registration form (assume the registration form is located in a file called register_form.php). Ideally we would want to display an error message, but for the sake of example we will keep it simple. We also check if the username exceeds 30 characters. Even though we set the form to allow no more than 30 characters it is important that we check as well; it is possible (and very simple) to bypass the limit imposed in the html code. In addition to this you should check for any other constrains you have placed on your data (valid characters, minimum length, etc).
Hashing
As I mentioned earlier, the length of the password varchar in the database is significant. This is because we are not actually going to (and never should) store the password in the database. We are going to store an sha1 hash which is a string always containing 40 characters. In simple terms a hash is an algorithm that maps inputs to outputs in a deterministic way. Meaning that given an input the algorithm will always produce the same output. Sha1 is an algorithm that outputs a 40 digit hexadecimal value. As in most cases, given the output of the sha1 algorithm it is not easy (though possible) to determine the input. In PHP we can get the sha1 hash of our password like this:
regsiter.php (part 2)
$hash = sha1($pass1);
Pass the Salt
As I mentioned before, there is no simple way to determine the input of the sha1 algorithm from the output. It is, however, possible through brute force or more complicated means. One way to improve the security of your users’ passwords is to use a salt. A salt is just a random string of characters that is appended to the hash, which is then hashed again.
regsiter.php (part 3)
//creates a 3 character sequence
function createSalt()
{
$string = md5(uniqid(rand(), true));
return substr($string, 0, 3);
}
$salt = createSalt();
$hash = sha1($salt . $hash);
Some people will tell you that a salt is not necessary, but it certainly doesn’t hurt to use one.
Now comes the database portion of our code, for this I will assume we are using a mysql database. I will also assume the database host, name, user, and password. I won’t anticipate any database connection errors in the code, but you should.
register.php (part 4):
$dbhost = 'localhost';
$dbname = 'tinsology';
$dbuser = 'tinsley';
$dbpass = 'trueblood'; //awesome tv show
$conn = mysql_connect($dbhost, $dbuser, $dbpass);
mysql_select_db($dbname, $conn);
//sanitize username
$username = mysql_real_escape_string($username);
$query = "INSERT INTO users ( username, password, salt )
VALUES ( '$username' , '$hash' , '$salt' );";
mysql_query($query);
mysql_close();
header('Location: login_form.php');
In the above code we establish a connection to our MySQL database and add our new user to the users table. Then we redirect to our login form. Notice that we call the funciton mysql_real_escape string. This function helps to prevent SQL injections by escaping the input. Using an abstraction layer like PDO will also help to prevent this. Now that we’ve processed our registration data we can write the code to process our login data.
Logging in
Seriously this time. Our login processor will pull the login data from post and compare it to the database values.
$username = $_POST['username'];
$password = $_POST['password'];
//connect to the database here
$username = mysql_real_escape_string($username);
$query = "SELECT password, salt
FROM users
WHERE username = '$username';";
$result = mysql_query($query);
if(mysql_num_rows($result) < 1) //no such user exists
{
header('Location: login_form.php');
}
$userData = mysql_fetch_array($result, MYSQL_ASSOC);
$hash = sha1( $userData['salt'] . sha1($password) );
if($hash != $userData['password']) //incorrect password
{
header('Location: login_form.php');
}
//login successful
As I mentioned before, this is a stripped down example. Ideally you would not have the code you use to connect to your database in each file, but rather in a single file or function that you could include. In addition to this you should maintain a session throughout the process in order to store and report error message, as well as other useful data. The most important thing to remember when creating a login system is that you should never trust your users. Validate all user input, protect against SQL injections, and never store raw passwords in the database.
P.S. Session Control
Added December 15th 2009
All of the above code illustrates how to register a user, and allow them to login. There is, however, one fundamental piece that is missing: session control. In order for a login system to be useful, it must provide some means to distinguish between a logged in user and a non-logged in user across the entire site. Sessions are the means by which we do this. What we want to do is, after a user has successfully logged in is indicate, using a session variable, that the user has done so.
Accessing Session Data
Activating and managing sessions in PHP is very straightforward, you only need one function to both create and recall a particular session: session_start(). Here is a generic example of how to use session_start to store session data:
page1.php
session_start(); $_SESSION['foo'] = 'bar';
page2.php
session_start(); echo $_SESSION['foo']; //will output bar
In the above example you can see that in page1 we start a session and assign the session variable “foo” the value “bar”. If the user visits page2 sometime there after the session will be recalled and the value of foo (bar) will be echo’d. Notice that session_start creates a new session if one does not exist or recalls that session if it already exists.
There are a few subtleties relating to session_start that are important to remember. You must call session_start before any headers are sent to the browser. This means that your script cannot have any output or calls to header() (or any other function that sends headers) before calling session_start. The simplest way to avoid this problem is to call session_start before anything else. One common case where this problem can occur is when some code that normally wouldn’t have any output generates an error or warning prior to calling session_start. The default error handler will automatically output any error messages to the browser.
Generally (though not necessarily), a session lives as long as the browser remains open.
Using Sessions in Our Login System
There are three basic functions we want to incorporate into our login system: validating a user (i.e. indicating that user has logged on), checking if a user is logged on, and logging a user out.
Validating a User
function validateUser()
{
session_regenerate_id (); //this is a security measure
$_SESSION['valid'] = 1;
$_SESSION['userid'] = $userid;
}
This function simply sets the session variable ‘valid’ to 1. You may also want to use this function to store certain variables. It is a good idea to store frequently accessed data about a particular user (such as a user id, username, NOT a password or sensitive data). The $_SESSION['userid'] = $userid; line is an example of how to store user info. Additional information about session security will be provided in the following section.
Checking if a User is Logged On
function isLoggedIn()
{
if($_SESSION['valid'])
return true;
return false;
}
This function simply checks if the session variable ‘valid’ is set to 1.
Logging Out
function logout()
{
$_SESSION = array(); //destroy all of the session variables
session_destroy();
}
When it is time to log a user out, we destroy the session. All of these functions assume session_start has already been called.
Now it is time to incorporate this into our login script:
session_start(); //must call session_start before using any $_SESSION variables
$username = $_POST['username'];
$password = $_POST['password'];
//connect to the database here
$username = mysql_real_escape_string($username);
$query = "SELECT password, salt
FROM users
WHERE username = '$username';";
$result = mysql_query($query);
if(mysql_num_rows($result) < 1) //no such user exists
{
header('Location: login_form.php');
die();
}
$userData = mysql_fetch_array($result, MYSQL_ASSOC);
$hash = sha1( $userData['salt'] . sha1($password) );
if($hash != $userData['password']) //incorrect password
{
header('Location: login_form.php');
die();
}
else
{
validateUser(); //sets the session data for this user
}
//redirect to another page or display "login success" message
Now we can use the isLoggedIn function to determine if a user is logged in and act accordingly:
membersonly.php
session_start();
//if the user has not logged in
if(!isLoggedIn())
{
header('Location: login.php');
die();
}
//page content follows
A Note About Session Security
By default, sessions are cookie based. This means that a particular session is associated with a user by use of a cookie. This being the case, there are certain vulnerabilities that arise. There are a variety of methods by which a session can be hijacked (XSS for example; a javascript injection can cause a user to give up their session id). Unfortunately you cannot expect to eliminate the possibility that a user has hijacked someone else’s session. Further research into the subject may yield a few tricks, but ultimately the best practice is to be cautious about certain tasks. If a user wants to change their password (after logging in), require the old password and whenever possible avoid displaying sensitive data.
Earlier I mentioned that you shouldn’t use session variables to store sensitive data. It is important to remember that session data lives on the server, this means that a user cannot directly view or modify session data. On a shared server, however, other users of that server may be able to access this information.
Just a reminder, all of the code examples I provided are exactly that: examples. They are meant to serve as a starting point, and hopefully shed some light on a few key concepts, but they are not is a real world implementation. I don’t recommend, for example, using header() to bounce your users around to different pages; there are better methods that reduce the number of page loads and give the user a more fluid experience. Using this method, however, simplifies things and makes the examples easier to follow.
Related posts:
Thanks for the excellent article- there are some subtleties I didn’t know about like session_regenerate_id.
NOW- I need to implement remember me (auto login) the “right” way! That would be a nice tutorial. Thanks!
Great Article… The only question that I have is would you put a username and password to the database into a file.
Would people be able to create there own users if they used a site copier.
I’m not quite sure what you are trying to say.
Are you asking if you can use a flat file to store user info? The answer is yes but I wouldn’t recommend it.
I’m also not sure what you mean when you say would you be able to create users with a site copier. I’m not sure what a “site copier” has to do with user management.
I’m fairly certain the gentlemen is assuming that someone using some script to ‘wget’ or ‘cURL’ each page in a directory would attain the ability to connect to your database via perusing the PHP code {for database info}; as you well know, he is mistaken. Unless there is some way to directly ‘download’ a PHP/CGI/JS document without first parsing it outside of direct FTP/SSH access that I’m not aware of, that is…
Also, thanks for the excellent tutorial!
Hi, great article! It’ll help me start to create an admin login system for a CMS I’m building from a variety of tutorials.
Anyway, here’s my question…
Using an OOP approach as you have above, what would the best way of creating a OOP templated admin area be? Could you advise? I envisage an admin login page which opens out into an admin cpanel with links to different aspects of my CMS.
Any help would be awesome! Cheers, RJP1
Using OOP doesn’t change much with regard to the overall approach of a login system. In fact, in my opinion, there is no need to incorporate much of the code I posted into objects, with a few exceptions. Since PHP gives you a choice when it comes to using objects (unlike Java) I recommend you exercise your ability to make that choice and use it only when you feel it is appropriate or necessary.
For instance, it is likely that you will have some kind of database class that will handle your database operations throughout your application. You may also have a user object that contains all of the information pertaining to a particular user. In this case, when a user logs in, you use the database object to run your queries and you create a user object that can then be stored in a session once the user is logged in. The bulk of the code will remain unchanged.
I personally don’t see any advantage in meticulously incorporating all of the code into objects.
This is a great article. Thanks a million.
One thing I noticed though, on line 10 of the first part of “Logging In”, you included a semicolon in the string –
WHERE username = ‘$username’;”;
Thanks again man, it’s a HUGE help.
SQL statements end with a semicolon just like lines in PHP code do. Technically it isn’t required unless you are executing multiple statements, but I’m in the habit of always ending statements with a semicolon.
Wow. Thanks. While I’ve figured most of this already, I needed something to launch me into actually implementing it in PHP. Great article.
Great starting point! Thanks
Thanks for this, very useful. I am already using it on a project!
Forgive my lack of knowledge (still very new to this), but when the user posts the login form is the login information passed to the server in clear text to be processed?
In a standard implementation would these pages then need to be run inside a secure connection, or is there some inherent security that I’m missing?
Unless you are using ssl/tls (HTTPS) data sent over the network will be unencrypted. In most cases, whether or not you are using https is transparent to PHP; code that works for http connections will work for https as well. In most cases you would only use https if you were dealing with sensitive data: credit card transactions, accessing your bank account, etc. Large sites such as facebook tend to use https as well.
To answer your question, you don’t need a secure connection to implement a login system, but it does add an extra layer of security. For a small site it is probably overkill and considering that a ssl certificate ranges from $100-$400 a year it may not even be feasible.
Quote: “…And the main reason: This is not a working implementation. I don’t mean for you to copy this code and put it into production. It is meant to demonstrate the concepts. It is meant to be a reference for creating a login system of your own.”
People who like me are looking for a login code, are hoping for a usable article, those who can code will do it themselves..
If you aren’t willing or able to take code and adapt it to your purposes, rather than copy and paste it out of the box, then why (and how for that matter) are you creating a login system? The article is meant to be a reference and assumes some coding experience. Knowing how to code isn’t the same as knowing how to create a login system (or anything else for that matter).
Great article! Being new to implementing login systems, this article thought me the basics I needed to get started. Thanks for sharing!
Your subject for this code is “Creating a Secure Login System the Right Way” yet you post insecure code. WTF? In your own words you say “you should never trust your users. Validate all user input, protect against SQL injections”, yet you don’t provide any sanitizing protection for the password input from the user. Newbies using your “secure” code will now be open to an sql injection attack.
Did you read the code carefully? The raw password never touches the database… it is hashed. There is no need to use escape_string or any other sanitizing on hashed data. The ONLY user input that touches the database is the username and that IS sanitized. In addition to this I point out PDO as an alternative to using the mysql_ family of functions, which is inherently more secure.
My bad! Feel free to delete my dumbass post. Thats what I get for writing and not sleeping. I take it all back!
I blindly missed the WHERE username = ‘$username’ and assumed it was WHERE username = ‘$username’ AND password = ‘$password’ which WOULD have been a problem.
Hi,
Could you maybe post or email me entire script in a zip file?
That would be really really great!
Thanx in advance!
/Nookie
Normally I would, but not in this case for three reasons:
If I post a zip file of this script I will have to update it whenever I updated this post
All I would be doing is copying the code I posted here into a few files
…And the main reason: This is not a working implementation. I don’t mean for you to copy this code and put it into production. It is meant to demonstrate the concepts. It is meant to be a reference for creating a login system of your own.
Thanks for the very good example. How ever I don’t know if any one experienced a crash with the registration form. I’m using Apache 2.2 with PHP 5 (WAMP configuration). It seems good practice to use mysql_close($conn); instead of mysql_close(); – Refer to register.php (part 4)
You are the first person to report a ‘crash’ as far as I’m aware. What exactly do you mean by crash? Is there an error message? Also, regarding mysql_close(), specifying the connection handle is only necessary if multiple connections are open, otherwise the behavior is identical to calling it without any parameters.
Hi Tinsley
Sorry for not being specific. I tested it again with mysql_close(); Ite gave me the following error:
Apache HTTP Server has encountered a problem and needs to close. We are sorry for the inconvenience. (One of those send / don’t send error reports).
The error signature contains the following information:
szAppName : httpd.exe
szAppVer : 2.2.14.0
szModName : php_mysql.dll
szModVer : 5.3.0.0
offset : 00002072
I did debug the code and the error did occurred at the mysql_close();. I did a google search and found other developers had a problem with this in the past and stated to use the connection handle variable to prevent such an error.
I’ve looked further into the issue and it appears that the problem stems from a bug in PHP 5.3.0 ( http://bugs.php.net/bug.php?id=48754 ); specifying the connection handle is a workaround for that bug. Upgrading to 5.3.1 should resolve the issue and you’ll be able to use mysql_close() without parameters.
[...] I just added a section about session management to Creating a Secure Login System the Right Way. [...]
Great article!! Very concise and easy to read… and of course use. This is exactly what I have been looking for.
I’m getting a 40-digit value from the sha1 hash. I can make this work by modifying my db to accept it, but is there something I’m doing wrong that it’s not coming out 28 digits?
Oh, and great article by the way.
It was exactly what I was looking for today.
That was my mistake. Standard sha encryption returns a 28 digits hexadecimal number, but for whatever reason it is 40 digits in PHP.
Thanks for this, much appreciated! I’m creating a little app with a login tool so this is useful.
And to follow up what Rob said, the maxlength attribute only asks the browser prevent the user form inputting a long string. The form data can’t be trusted since it can be cirucmvented.
Which is exactly what I mentioned later in the article.
Great article… One correction though. Specifying a size attribute doesn’t keep the user from typing more text… The attribute you want to use is maxlength.
Rob
My mistake, that’s what I get for writing instead of sleeping.