Creating A PHP Login
Creating a user login is something very commonly however all to often done poorly and not very securely. In this guide we’ll walk though some key points with example code explaining the reasoning on why each part is essential.
First off we’ll cover some important ground rules when it comes to writing a login with PHP.
Never store passwords, only store hashes of passwords
Without going into too much detail a hash is a function that takes a string of any length as an input and returns a fixed length hash value. It is a “one way” process so if you have the hash you won’t be able work out the data that was input. In the context of storing user credentials it’s important to store hashes instead of passwords because if your user table was ever compromised for example a backup was stolen/lost, an sql injection attack or a rouge employee then they wouldn’t be able to see your users passwords or be able to login to your application.
One common way of hashing passwords in PHP was a hash known as MD5, while there are still many websites that use MD5 it’s now considered not very secure. The reason for this is because computing power has moved on so much in the last 10/15 years. It’s now possible to brute force and build dictionary’s of common passwords (known as rainbow tables). While it’s still true that you can’t reverse a hash using maths you could build a database of words then apply the hash to every word until you get a match.
So to counter this we need to use a much slower hashing method and apply something known as a salt. First off it may sound counter intuitive using something that purposely uses more CPU resource however it makes it much more expensive/time consuming for cyber criminals to build dictionaries/brute force your passwords. The current best practice is not to use MD5 or SHA-1 instead opt for SHA-256 or higher.
What is a salt?
A salt is a random piece of data that is used as an additional input to the one way function for the password hash. The primary function of salts is to defend against dictionary attacks versus a list of password hashes and against pre-computed rainbow table attacks. So to make it easier to understand all it is is an extra string that’s added to the password before you hash it to increase the complexity of the password.
One other thing to remember when it comes to salts is that you should use a different salt for each password you hash, the reason for this isn’t because it necessary makes it harder for someone to crack but if you imagine this type of situation. Your user table becomes compromised and the attacker does a group by to count how many of the hashes are the same, if they end up finding several accounts with the same hash they then know that those users have used the same password which also implies its a commonly used password. With that information they can then specifically target those accounts with a much greater chance of gaining unauthorised access to your site/application.
The good news is all this is very easy to implement as there are some new PHP functions (as of PHP 5.5) to help, so there’s no excuse not to use them.
How to save a password hash in PHP:
<?php $userPassword = "thePasswordString"; $options["cost"] = 10; $passwordHash = password_hash($userPassword, PASSWORD_DEFAULT, $options); echo $passwordHash; // Will display something like $2y$10$dippZb2QQBJII3mEa/Gh6.aP.DpcuIOQiHGWGfVIGJzJUmDkmc5yq ?>
The important function in the code above is password_hash() it accepts 3 parameters, the first is the password you want to hash, the second is the algorithm you wish to use (by default this will be the bcrypt algorithm but it may change over time when stronger algorithms are released) and the third is the support options. You may notice we’re passing in 10, that is a good baseline cost, if you increase that number it will use more resource and be slower, if your decrease it it will use less resources and be quicker.
You may notice we’re not passing in a salt in the example, if you want you can force your own salt by passing it as an option like:
<?php $options["salt"] = "MyRandomSalt"; ?>
However by not including the salt PHP will generate a random one for you and include it in the hash it outputs making it possible to verify against when you come to authenticate a user later on.
The output will look something like “$2y$10$dippZb2QQBJII3mEa/Gh6.aP.DpcuIOQiHGWGfVIGJzJUmDkmc5yq”. The first part of that “$2” is the crypt, the second part “$10” is the cost, the third part is the salt and the forth is the hash.
How to authenticate a user login
We’ll assume you have checked to see if the user exist and you have retrieved the hash for that users password from the database, please take note of our warning about SQL Injection when using user input in any SQL statements. The code below shows how to compare a stored hash that was previously generated using the method above with the password the user entered.
<?php $userPostedPassword = $_POST["password"]; if(password_verify($userPostedPassword, $userRow["hassedPassword"])){ // authentication successful $_SESSION["UserID"] = $userRow["UserID"]; $_SESSION["login_sha"] = hash('sha256', $userRow["UserID"] . $_SERVER["REMOTE_ADDR"]); }else{ // authentication unsuccessful header('Location: https://www.yourdomain.co.uk/login/'); } ?>
You may also notice we’re storing the user ID along with the IP address of the user as a hash in the session, this is to help prevent session hi-jacking which we’ll cover next.
Checking if a user is currently logged in
Because you won’t want to ask the user to login for every page they visit you’ll need a way of knowing if the user has already logged in or not. This is normally handled by using PHP sessions, but this can still present an issue if the users session gets hi-jacked it would allow the attacker to effectively have access to everything your user would. Sessions can be hi-jacked a number of ways but you can defend your site/app against them by doing the following.
<?php session_start(); $userID = $_SESSION["UserID"]; $login_sha = $_SESSION["login_sha"]; $ip = $_SERVER["REMOTE_ADDR"]; $login_sha_verify = hash('sha256',$userID . $ip); if((!$userID) OR ($login_sha_verify !=$login_sha)){ // Not a valid session header('Location: https://www.yourdomain.co.uk/login/'); exit(); } ?>
Here you are also making sure that when the session is checked it must also come from the same IP address that was used when the user logged in.
Preventing brute force logins
Any login page is potentially vulnerable to something know as a brute force attack, it is quite a crude method of attack and you don’t need a huge amount of knowledge to perform the attack. If you haven’t come across it before it’s basically the technique of guessing the username and password until you eventually find a valid login, at the lower end it may just be someone guessing and typing passwords at the higher end it could be a script with multiple threads running through huge word lists of automatically generated word lists constantly for days/weeks/months to find even the strongest of passwords. Below is a screenshot of a brute force attack on a WordPress login page.
You may see some sites suggesting CAPTCHA is a way to stop brute force attacks but CAPTHA’s can be beaten by those who can code well and it also makes the user experience much worse, particularly with some of the harder to read CAPTCHA’s or when users are using a mobile device.
The best way to stop a brute force attempt on your login page is to record each login failure along with the username and IP address. Then make sure before you authenticate the login you check to make sure there haven’t been a certain number of login failures (say 5 for example) from that username or IP address in the last 5/10 minute period. If there have been more than your threshold display a message saying the account has been locked for x number of minutes. If you wanted to increase your protection you could add an intrusion detection alert for you system administrator to check out if there is an IP address or username that is constantly getting locked.
Although this technically doesn’t stop anyone from brute forcing you it massively increase the time it would take for an attack to be successful and effectively stops it dead in the water.
Note on SQL injection
We haven’t covered SQL injection in this guide because it’s a whole other topic by itself, but whenever you have data that originated from a user it must be treated as hostile! It’s good practice to use prepared statements using PDO and to make sure you define the character set (such as UTF8) when you open your database connection.