Email Verification With Laravel
Email verification is a common step in the registration process of many websites, however, it is not always clear why it is required. Here are a few reasons you may want to use email verification in your own application:
- For an application in which registered users can post public messages (a forum, comments section, etc.), the addition of email verification before a user can sign in, can vastly decrease the ability of bots to post spam.
- If an application intends to send email to the user that may contain sensitive information (password reset links, etc.), then by implementing email verification, the application can verify that no mistakes were made when the email address was first entered.
- By verifying the users email before sending anything else, the application can avoid sending unwanted messages to an email address which has been entered by mistake or maliciously.
- Sending a verification email can increase the chances that the user will whitelist the email address on any spam filters that may be in place. This can be useful if the application will be sending a large quantity of email in the future.
- For some applications none of the above are applicable and asking for email verification is used as a means of creating 'security theatre' that the application may hope will give it an air of trustworthiness.
Only add email verification to your registration flow if you believe it benefits your application in some way and remember, the quicker it is for a user to get started with your application, the better their experience will be.
In the rest of this article I will cover a simple implementation for email verification within a Laravel application. The completed code for this demonstration can be found on Github.
User Table Migration
We need to create just two fields in addition to the fields that are standard in most users tables (username, email, password, etc.). Firstly, we need a boolean field 'confirmed' to keep track of whether a user has confirmed their email address, this will be set to false by default.
The second field that we require is a confirmation_code
string field. When a user is signed up we set this field to a random string, an email is then sent to the user asking them to confirm their account by following a link to /register/verify/{confirmation_code}
. When a user follows this link, we take the passed in confirmation code and search for it within the users table. If a matching confirmation code is found we set the confirmed field for this user to true and set the confirmation code to null
.
The migration below is for a very basic user table. Notice that the confirmed field is set to false by default and that the confirmation_code
is nullable.
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint; class CreateUsersTable extends Migration { public function up() { Schema::create('users', function(Blueprint $table) { $table->increments('id'); $table->string('username')->unique(); $table->string('email')->unique(); $table->string('password'); $table->boolean('confirmed')->default(0); $table->string('confirmation_code')->nullable(); $table->rememberToken(); $table->timestamps(); }); } public function down() { Schema::drop('users'); }
}
Registering A User
Now that the users table is set up we can begin adding our registration functionality. For the most part this is the same process as normal. When the user submits the registration form we validate the user input and redirect back with the errors if the validation fails.
If the validation passes then we need to create a random string that can be used as the confirmation_code for the user. We can do this very easily using the Laravel helper function str_random()
which takes the length of the string we wish to generate as an argument. With the confirmation code generated, we now have all the information we need to create the user, just remember that if you are use mass assignment with User::create()
, you need to set the $fillable
property on your user model to contain username, email, password and confirmation code.
Once the user has been created the only thing left to do is to send them their confirmation email. The email can contain whatever welcome message you wish, the only thing that must be included is the confirmation link. As stated earlier, the convention that we are going to follow for the confirmation route is /register/verify/{confirmation_code}
. We will use Laravel's Mail::send()
and create a very basic email template that will take the confirmation code and output a link to the confirmation url that the user needs to visit. For a basic guide to sending email with Laravel read this.
use Illuminate\Support\Facades\Input; class RegistrationController extends \BaseController { public function store() { $rules = [ 'username' => 'required|min:6|unique:users', 'email' => 'required|email|unique:users', 'password' => 'required|confirmed|min:6' ]; $input = Input::only( 'username', 'email', 'password', 'password_confirmation' ); $validator = Validator::make($input, $rules); if($validator->fails()) { return Redirect::back()->withInput()->withErrors($validator); } $confirmation_code = str_random(30); User::create([ 'username' => Input::get('username'), 'email' => Input::get('email'), 'password' => Hash::make(Input::get('password')), 'confirmation_code' => $confirmation_code ]); Mail::send('email.verify', $confirmation_code, function($message) { $message ->to(Input::get('email'), Input::get('username')) ->subject('Verify your email address'); }); Flash::message('Thanks for signing up! Please check your email.'); return Redirect::home(); }
}
and the simple verification email using blade:
<!DOCTYPE html>
<html lang="en-US"> <head> <meta charset="utf-8"> </head> <body> <h2>Verify Your Email Address</h2> <div> Thanks for creating an account with the verification demo app. Please follow the link below to verify your email address {{ URL::to('register/verify/' . $confirmation_code) }}.<br/> </div> </body>
</html>
Confirming The User
To complete the confirmation process the user must follow the link that is sent to them in their welcome email. A corresponding entry should be included in the routes.php
file:
Route::get('register/verify/{confirmationCode}', [ 'as' => 'confirmation_path', 'uses' => 'RegistrationController@confirm'
]);
The confirm method in the registration controller needs to do a few things. Firstly, we need to find the user to which this confirmation code belongs. If no confirmation code is included or the code does not belong to any user then we will redirect the user back to the home page.
If a user is found then we set their confirmed field to true and set their confirmation_code
to null. This is done so that on the off chance the same confirmation code is generated and given to two separate users, then as long as the first user verified their email address they will no longer have this code set and no problems should arise. The user is then saved and redirected to the login form along with a flash message thanking them for verifying their email and notifying them that they can now sign in.
<?php class RegistrationController extends \BaseController { public function confirm($confirmation_code) { if( ! $confirmation_code) { throw new InvalidConfirmationCodeException; } $user = User::whereConfirmationCode($confirmation_code)->first(); if ( ! $user) { throw new InvalidConfirmationCodeException; } $user->confirmed = 1; $user->confirmation_code = null; $user->save(); Flash::message('You have successfully verified your account.'); return Redirect::route('login_path'); }
}
Logging A User In
The only addition that needs to be made to the authentication system is to check whether the user is confirmed prior to logging in. Laravel's Auth::attempt()
function can take extra conditions which must be true for a successful login attempt. This means we can add 'confirmed' => 1
to our credentials array to ensure that a user is confirmed before they can log in.
<?php class SessionsController extends \BaseController { public function store() { $rules = [ 'username' => 'required|exists:users', 'password' => 'required' ]; $input = Input::only('username', 'email', 'password'); $validator = Validator::make($input, $rules); if($validator->fails()) { return Redirect::back()->withInput()->withErrors($validator); } $credentials = [ 'username' => Input::get('username'), 'password' => Input::get('password'), 'confirmed' => 1 ]; if ( ! Auth::attempt($credentials)) { return Redirect::back() ->withInput() ->withErrors([ 'credentials' => 'We were unable to sign you in.' ]); } Flash::message('Welcome back!'); return Redirect::home(); }
}
Notes
The code in this tutorial is simplified to make the article easier to follow, if I were using this code in production there are a number of changes I would make. The controllers contains far too much domain logic which I would extract to either service classes or using a command based architecture. In addition to this I would add a layer of abstraction to my database layer with the use of repositories.
I would either extract validation out to it's own service or if I was using a command architecture I would decorate the command bus to allow for validation prior to the command handler being executed.
There are no filters for any of the controller routes, I would want to make sure that a logged in user could not access the log in or registration pages. We could do this by applying the auth filters in the controller or in the routes.php
file itself.
Finally, I would want some functional tests backing up this code. I would want separate test for user registration, user email confirmation and user log in and if I was using repositories to have integration tests backing these up.
You may have noticed in the controller methods the use of a flash facade. This is thanks to the laracasts/flash package which gives a clean interface for specifying flash messages. If you are a Laravel user and do not have an account with Laracasts then you owe it to yourself to get one. The $9 a month fee will pay itself back within minutes.
If you have any questions then please leave a comment below and I will do my best to help out.