This chapter is about means to protect your application from anonymous users who could deliberately delete random data or pollute your application with spam messages. Until now, your application was publicly available to online audience without any possibility to control who is working with stored data. I want to show you how to store user account data securely (especially passwords) and how to verify (authenticate) a user which is trying to log into your application. I will not talk about different levels of user permissions because it would complicate things a lot – such feature is called authorisation.
It is not a safe approach to store passwords in their plain-text form. Such passwords can be viewed by anybody who has access to your database (maybe now it is only you, but in future it can be some of your colleagues or even your employees). A password is always saved in hashed form (a hash is a result of function which outputs a unique strings for different inputs and it is not trivial to reverse this process – i.e. to calculate original password from a hash).
Examples of hash function outputs in PHP for word “cat”:
Function password_hash()
is a bit different because its output is not always same like with the md5 or
sha1 functions. It is caused by a salt which is a sequence of random characters. The salt is used to make
hashes unique to avoid attackers from searching hashes online or using vocabulary attacks (try to
search for md5 hash e246c559bf94965d89cf207fc45905bc using Google –
d’oh). A salt is stored directly in the result of password_hash()
function.
To verify a password generated by password_hash()
use password_verify()
function.
You can also use a salt with the md5 or sha1 but you have to handle it by yourself (you will need another column in your database table to store it).
It is impossible to show/send user his original forgotten password due to hashing (but the security benefits of hashing
are more significant). If you need to have the ability of self managed passwords restoring in your application, you
should send a new password to email specified during registration process. You can use PHP’s mail()
function.
Even better way is to send unique password reset request link to his email, so the password is in fact restored
only when the user clicks this link from his email account.
This makes the email address of a user also a very sensitive information. Change of an email address should only be possible after confirmation of ownership of current email address – send unique token to his current email address to authorize changes.
To store a password during registration process:
hash = sha1(randomSalt + registrationPassword)
To verify a password when user tries to log-in (first fetch hash and salt from database):
if(sha1(databaseSalt + providedPassword) == databaseHash) {...OK...}
The probability that concatenation of user’s password and a random string used as a salt
will yield results in vocabulary search is very small. Yet use of sha1()
or md5()
functions
is strongly discouraged because powerful CPUs or GPUs can generate millions of hashes per second and
they are capable of breaking md5 or sha1 hashes within reasonable time (days). Recommended function
password_hash()
is also quite slow (by design – this is one of few instances when we want our
algorithms to be slow).
One of the reasons why any web application sends you a new password instead of yours when you forgot it, is hashing. They do not store your password in plain text form.
Create a table with login or email column and a column to store password in hashed format.
If you plan to use password_hash()
function, you do not need another column for salt.
Remember that login or email column must have unique constraint to prevent duplicate user accounts.
For PostgreSQL:
For MySQL:
Registration is actually not very much different from any other record insertion. Only difference is that you have to validate match of the passwords and calculate the hash. After successful insertion of the account record redirect the visitor to the login page.
It should have an input for login or email and two inputs for password verification (all inputs are required).
You can use Bootstrap CSS styles. Prepare a place to display error message and
remember to prepare form
data array to display values in case of failure.
File templates/register.latte
:
Add a GET route to display this template.
File src/routes.php
:
Make a new POST route to process registration of new users. Use password_hash() function. Read the documentation because this function requires actually two input parameters. Second one is the algorithm which is used for password hash calculation.
File src/routes.php
:
After successful registration, a record representing user account in the database should look like this:
User verification is also not a big problem – a person who wishes to log-in into your application has to visit login page with a simple two-input form. After he fills and submits the form, he is verified against the database. If an existing account is found and passwords match, your application can trust this user.
Actually there were cases when a user logged into another user’s account by a mistake – two different accounts had same passwords (not even salt can solve this situation). There are also online identity thefts when user’s password is compromised and used by someone else to harm original person. You can add another tier of user authentication, e.g. send an SMS to his cell phone to retype a verification code or distribute user certificates.
Create a login form and a Slim routes to process login information. You can make error message a bit confusing to obfuscate existence of user accounts (sometimes you do not wish to easily reveal users of your app – especially when you use email address as login). For now, do not bother yourself by the fact that the confirmation is displayed only when the user sends his credentials. We will handle persistence of authentication flag later.
File templates/login.latte
:
File src/routes.php
:
Now we need to verify user information against the database and display errors when there are some. We will use the password_hash() counterpart function password_verify() similarly as in registration route:
File src/routes.php
:
Add a message slot to display errors in the template.
File templates/login.latte
:
You probably noticed that there is no way to tell if a user has authenticated in subsequent HTTP requests due to stateless nature of HTTP protocol. To safely store login information you would probably want to logically connect subsequent HTTP request from one client (internet browser) and associate these requests with some kind of server storage. That is exactly what sessions are used for. A session is a server-side storage which is individual for each client. Client holds only unique key to this storage on its side (stored in cookies). Client is responsible for sending this key with every HTTP request. If the client “forgets” the key, data stored in session is lost. The key is actually called session ID.
To initiate work with session storage you have to call PHP function session_start()
in the beginning of each of your script (before you send any output, because session_start()
sends a cookie via
HTTP headers).
In PHP, there is as superglobal $_SESSION
array which is used to hold various data between HTTP request. These
data are stored on a server and cannot be modified by will of a visitor – it has to be done by your application’s
code. The $_SESSION
variable is initialized and eventually filled by session_start()
function.
Use $_SESSION
variable to store authenticated user’s data after login. Note that there is already a line with
session_start();
function in the public/index.php
script.
File src/routes.php
(final version):
You can prevent anonymous users to access all your application’s functions or just selected ones. If a visitor tries to access prohibited function without authentication, he should be redirected to the login page.
Write a middleware function which will verify
presence of user’s data in $_SESSION
array and redirect to login route if no such data is found. The
is automatically executed before (or after) a route. You can choose to have a global (application) middleware
for all routes or a middleware for a group of routes or a middleware for a particular route. We do not want
to block all routes and therefore we will create a middleware only for a certain group of routes.
The middleware handler is provided with three input parameters, two of them are already familiar, it is the
$request
and $response
objects. The third one is the $next
callback which represents the next action (a route
that should be called according to URL or another layer of middleware). The middleware can decide whether to call
the $next
callback or return $response
like you do in ordinary route. You have to pass the $request
and
$response
objects to the $next
callback. The middleware can also be executed after the route itself – all
you have to do is call $next
handler beforehand and then modify returned value which is a modified $response
.
Here is an example how to protect a new route with user profile information using such middleware:
File src/routes.php
:
File templates/profile.latte
:
Another option is to store the middleware handler into a variable and add it to selected routes manually:
$authMiddleware = function($request, $response, $next) { ... return $next($request, $response); ... };
$app->get('/route1', function(...) {...})->add($authMiddleware);
$app->get('/route2', function(...) {...})->add($authMiddleware);
Now you can see why is it important to give names to your routes. You can transfer all routes into this “auth”
group without additional changes. Result of this operation is that all these route URLs now start with /auth
string. Because you used named routes and {link ...}
macro, you do not have to change URLs in templates at all.
Because route /
named index
is on URL /auth/
(/auth
+ /
), you can create a redirect route which tells
new visitor’s browser to redirect either to index
or login
route.
If you think that your code is becoming a bit hard to read and maintain, and you are probably right, you can always
distribute individual or semantically similar routes into separate PHP files. You can then
include these files – this function simply reads and executes
the contents of given PHP script. Check out file public/index.php
which uses require
– a more strict version of include.
$app->get('/', function(Request $request, Response $response, $args) {
//redirect to actual index
return $response->withHeader('Location', $this->router->pathFor('index'));
});
$app->group('/auth', function() use($app) {
include('routes-person.php');
include('routes-contact.php');
//and so on...
})->add(function($request, $response, $next) {
//middleware code
});
Finally, we have to give our users an option to leave our application. A logout action is usually just deletion of
all user related data from $_SESSION
variable on server.
Sometimes you wish to leave some data in the $_SESSION
variable – the contents of shopping cart, for example.
Make a POST route which will handle logout. It is safer to use POST method because GET logout route can be easily
exploited via XSS and CSRF. Use session_destroy()
function. Redirect user to a public route of your application after logout.
File src/routes.php
:
Here is an example of logout form with a single button:
File templates/profile.latte
:
Make a link to logout route from main menu. You can display user’s login name inside the link. You can also pass user information into the template inside the middleware. Remember to hide the logout button when there is actually no user signed in.
User authentication or even authorisation is complicated. I demonstrated one of the easiest ways how to do it – you can also hardcode passwords into your source code (do not do that!). Keep in mind that weakest point of application is probably its user because people are lazy to fabricate new passwords for each website and they share some passwords with their family or friends. Security measures should also be designed according to the type of application you are developing (a bank account management application VS discussion board).
Passwords are sent over the network in plain text, it is a good idea to use HTTPS for (at least) registration and login pages to prevent attackers to sniff the password from network traffic. Still this does not prevent targeting users of your application by scam login pages which are used to steal their authentication information.
There is a lot more to explore: you probably know sites where you can persist your authentication for duration of days or even weeks – ours is forgotten as soon as the visitor closes his browser’s window. That can be achieved by setting special attributes to cookies which hold keys to a sessions. You also probably seen that some sites use global services like Facebook or Google to provide login functionality, which is good for users who do not want to remember too many passwords. Totally different approach of authentication is used for single page applications.
Remember that you are responsible for security of your application and also for the data of your users.