Login Form in PHP Example
In this example we will see how to make a login mechanism in PHP. The login systems is one of the most liked targets of attackers, so we have to be extremely careful with every aspect of the login.
For this tutorial, we will use:
- Ubuntu (14.04) as Operating System.
- Apache HTTP server (2.4.7).
- PHP (5.5.9).
- SQLite3, a lightweight and process-less DBMS. Credentials are almost always saved in databases, so we have chosen the lightest option for developing purposes. This not should be an option in production environments.
1. Preparing the environment
1.1. Installation
Below, commands to install Apache, PHP and SQLite are shown:
sudo apt-get update sudo apt-get install apache2 php5 libapache2-mod-php5 php5-sqlite sudo service apache2 restart
1.2. PHP configuration
We have to configure PHP to add the SQLite driver. Open /etc/php5/apache2/php.ini
, and add the following directive, if it not exists:
extension=sqlite.so
Don’t forget to restart Apache after doing any change.
Note: check the write permissions of your working directory, since the database will be placed in that directory.
2. How should passwords be stored?
After developing any login system, we first need to decide how the passwords are going to be stored. Let’s see which are the possible ways to store password, from worse option, to best:
2.1. Worse: plain text
Even if can be an obviousness, is something that must be said (mostly, because it’s still being done): please, don’t store passwords in plain text. At the same time a password is stored as plain text, it’s compromised. Anyone having access to the password storage (commonly, a database), will know the password, when is something that is thought to be known only by its owner. And if the system is compromised by the attacker, it would have all the passwords readable.
2.2. Not bad: password hashing
To avoid this, password hashing was designed. This consists on calculating the hash of the password, to store that hash. Hash functions are one-way functions, that is, if hf(password) = hash
, where hf
is the hash function, is computationally unfeasible to deduce password
from hash
. So, for a login, the stored hash has to be compared with the hash of the password provided but the user.
Password hashing is a much better password storing system that plain text, but keeps being vulnerable. Precomputed tables of hash values could be used (many of them are available already on Internet) to force the login, or to get the original password from its hash, also called digest. One of the features of hash functions is that they are extremely fast, and this, for login, is a disadvantage, since an attacker has more chances to attempt logins in less time.
2.3. Better: password hashing + salting
This solves, partially, the problem described in paragraph above. Salting consists on adding a random string of bytes (called salt) to the password before its hashing. So, same passwords, would have different hashes. To perform the login, the hash of the password provided by the user plus the stored salt for that user has to be calculated, and then compare with the stored one.
If an attacker uses a precomputed table of hash values, it still could attempt a brute-force attack, but it won’t be possible that easy to revert the hash to get the plain password, because the random salt has been part of the hash calculation.
In this case, to calculate the hash, we would do the following: hf(password + salt) = hash
.
Note: when calculating salts, strong random functions based on entropy should be used, not that weak and predictable functions like rand()
.
2.4. Even better: Key Derivation Functions
As we said above, password hashing plus salting helps to keep the passwords secret against precomputed table-like attacks. But an attacker could gain access to a system with a brute-force attack, without needing to know the password.
To avoid this, Key Derivation Functions (KDF) where designed. This functions are design to be computationally intense, so, needs much time to derive the key. Here, computationally intense, means a second, maybe something more. Actually, the concept is the same that in hashing + salting, but here another variable is used: the computational cost, which defines the intensity we mentioned above.
So, the operation would be: kdf(password, salt, cost) = dk
, where dk
is the derived key generated.
Let’s see a time comparison between a common hash function (SHA1, which is deprecated, but it’s still widely used), and a KDF (bcrypt):
sha1_vs_bcrypt.php
<?php $password = 'An insecure password'; $starttime = microtime(true); sha1($password); $sha1Time = microtime(true) - $starttime; $bcryptOptions = array('cost' => 14); $starttime = microtime(true); password_hash($password, PASSWORD_BCRYPT, $bcryptOptions); $bcryptTime = microtime(true) - $starttime; echo "sha1 took: $sha1Time s <br>"; echo "bcrypt took: $bcryptTime s <br>";
The output will be something like this (depending on the hardware):
sha1 took: 1.5020370483398E-5 s bcrypt took: 1.2421669960022 s
As you can see, we reduce significantly the attempts an attacker could perform, passing from 1.5*10-5 seconds, to 1,2 seconds. And, for a user, waiting a second for a login is almost imperceptible.
Is it possible to adjust the intensity the algorithm takes, as in line 9, to find the value that fits better with our hardware.
The derived password (the return value of password_hash()
function) will be similar to the following string:
$2y$14$WH1yQiP1naJD8b8lWOK1bOxGQUjgCpFwuzSKohGL/ZV1NaYYr5Cge
Which follows the following format:
$<algorithm_id>$<cost>$<salt (22 chars)><hash (31 chars)>
So, in that example, would be:
- Algorithm id:
2y
(bcrypt). - Cost:
14
- Salt:
WH1yQiP1naJD8b8lWOK1bO
- Hash, or derived key:
xGQUjgCpFwuzSKohGL/ZV1NaYYr5Cge
Taking this into account, for authenticating the user, we would have to extract the cost and the salt to reproduce the operation, to then compare the hashes. We will see this in the login implementation.
Note: in the password_hash()
function, in the $options
array, we can also specify a salt. If any salt is specified, the function will generate one. For creating salts, the recommended way is using mcrypt_create_iv()
function.
3. Creating users
Let’s do a simple script that allows to create user, to later test that our login system works correctly.
create_user.php
<?php require_once('db.php'); function checkGETParametersOrDie($parameters) { foreach ($parameters as $parameter) { isset($_GET[$parameter]) || die("'$parameter' parameter must be set by GET method."); } } checkGETParametersOrDie(['username', 'password']); $username = $_GET['username']; $password = $_GET['password']; $db = new DB(); $db->createUser($username, $password); echo "User '$username' has been created successfully.";
To create the user in the database, we developed a class named DB. We will see it below. Now, enter the URL to the script location:
127.0.0.1/php_loginform_example/create_user.php?username=myuser&password=mypassword
With the username and password you prefer.
4. Login
4.1. Form
A simple login form that asks for an username and password.
login_form.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Login form</title> </head> <body> <form action="login.php" method="POST"> <div> <label for="username">Username:</label> <input type="text" name="username" id="username" required> </div> <div> <label for="password">Password:</label> <input type="password" name="password" id="password" required> </div> <div> <input type="submit" value="Login"> </div> </form> </body> </html>
4.2. Form handling
To handle the form, we will create the following script:
login.php
<?php require_once('db.php'); /** * Checks if the given parameters are set. If one of the specified parameters is not set, * die() is called. * * @param $parameters The parameters to check. */ function checkPOSTParametersOrDie($parameters) { foreach ($parameters as $parameter) { isset($_POST[$parameter]) || die("'$parameter' parameter must be set by POST method."); } } // Flow starts here. checkPOSTParametersOrDie(['username', 'password']); $username = $_POST['username']; $password = $_POST['password']; $db = new DB(); $authenticated = $db->authenticateUser($username, $password); if ($authenticated) { $response = "Hello $username, you have been successfully authenticated."; } else { $response = 'Incorrect credentials or user does not exist.'; } echo $response;
Actually, only retrieves the parameters sent by the form, and calls DB class function authenticateUser() method. Let’s see now that class.
4.3. Login against database
The interesting part. This class is the one that interacts with the database, to perform the login, and also to create the users:
db.php
<?php /** * Methods for database handling. */ class DB extends SQLite3 { const DATABASE_NAME = 'users.db'; const BCRYPT_COST = 14; /** * DB class constructor. Initialize method is called, which will create users table if it does * not exist already. */ function __construct() { $this->open(self::DATABASE_NAME); $this->initialize(); } /** * Creates the table if it does not exist already. */ protected function initialize() { $sql = 'CREATE TABLE IF NOT EXISTS user ( username STRING UNIQUE NOT NULL, password STRING NOT NULL )'; $this->exec($sql); } /** * Authenticates the given user with the given password. If the user does not exist, any action * is performed. If it exists, its stored password is retrieved, and then password_verify * built-in function will check that the supplied password matches the derived one. * * @param $username The username to authenticate. * @param $password The password to authenticate the user. * @return True if the password matches for the username, false if not. */ public function authenticateUser($username, $password) { if ($this->userExists($username)) { $storedPassword = $this->getUsersPassword($username); if (password_verify($password, $storedPassword)) { $authenticated = true; } else { $authenticated = false; } } else { $authenticated = false; } return $authenticated; } /** * Checks if the given users exists in the database. * * @param $username The username to check if exists. * @return True if the users exists, false if not. */ protected function userExists($username) { $sql = 'SELECT COUNT(*) AS count FROM user WHERE username = :username'; $statement = $this->prepare($sql); $statement->bindValue(':username', $username); $result = $statement->execute(); $row = $result->fetchArray(); $exists = ($row['count'] === 1) ? true : false; $statement->close(); return $exists; } /** * Gets given users password. * * @param $username The username to get the password of. * @return The password of the given user. */ protected function getUsersPassword($username) { $sql = 'SELECT password FROM user WHERE username = :username'; $statement = $this->prepare($sql); $statement->bindValue(':username', $username); $result = $statement->execute(); $row = $result->fetchArray(); $password = $row['password']; $statement->close(); return $password; } /** * Creates a new user. * * @param $username The username to create. * @param $password The password of the user. */ public function createUser($username, $password) { $sql = 'INSERT INTO user VALUES (:username, :password)'; $options = array('cost' => self::BCRYPT_COST); $derivedPassword = password_hash($password, PASSWORD_BCRYPT, $options); $statement = $this->prepare($sql); $statement->bindValue(':username', $username); $statement->bindValue(':password', $derivedPassword); $statement->execute(); $statement->close(); } }
Remember when we said in 2.4 section that, for comparing the provided password with the string generated by the KDF algorithm, we would have to extract the cost and the salt from that string? Well, the password_verify()
function, in line 45, does this for us: repeats the operation for the provided $password
, extracting the algorithm, the salt and the cost from the existing derived key $storedPassword
, and then compares it to the original password that is the “reference”.
Note that, when querying the database, we use something called $statement. These are called Prepared Statements. We can execute directly a string SQL with a SQLite method called exec(), but this would be vulnerable against SQL Injection, and an attacker could gain access to the system injecting SQL commands. The Prepared Statement does not allow this, because it binds the parameters using a different protocol, and don’t need to be escaped looking for characters like '
.
If we try to fill the form with the credentials we created in section 3, we will receive the following message:
Hello <user>, you have been successfully authenticated.
Whereas, if we introduce incorrect credentials, we will see:
Incorrect credentials or user does not exist.
5. Summary
We have seen that there are different mechanisms to design a login system, analysing the weakness of each one, concluding that the KDFs are the most secure alternative currently, and how to implement it in PHP. We have also seen the need of the use of the Prepared Statements, not to allow hypothetical attackers to introduce malicious commands to gain access or to extract information.
6. Download the source code
This was an example of a login form in PHP.
You can download the full source code of this example here: PHPLoginFormExample
Where is the database users.db created after the files are executed
Please check the complete login and registration system developed in PHP @ a2zwebhelp.com
https://www.a2zwebhelp.com/php-login-registration-system