When it comes to user authentication and authorization, Laravel is one of the best frameworks out there. But when trying to setup authentication for the first time in Laravel, the process can seem a bit daunting, especially when you are trying to do something a bit more advanced than the basic authorization functionalities. According to the official documentation: “Laravel makes implementing authentication very simple”. I would add “Laravel makes implementing authentication very simple, if you know what you are doing”. This tutorial covers authentication and authorization in Laravel 5.3 by creating an application that allows users to register, login and retrieve their lost password/username. You can try out the demo here and you can download the source code from GitHub.
In short, we will build an application that allows people to register, and once they are registered, they receive an email to confirm their account. Once the user has activated their account, they can then login. We must also take care of users who forget their password or their username. Finally, we update the user through various email notifications if anything happens to their account. In short, here are the functionalities that we will be covering in our application:
- Users can register on the site and get an activation email
- Users will register using a username/password combination instead of a email/password confirmation
- Users can confirm their account through the activation email that contains an activation link
- Users can click on the remember me option to stay logged-in forever
- Users can request the system to resend the activation code in case they lose it.
- Users can login once they have confirmed their account
- Users can reset their password if they forget it.
- Users can aske for a username reminder if they forget it
UPDATE: I have now decided that I will be creating an Auth trilogy of tutorials for Laravel 5.3. The breakdown will be as follows:
- Part 1: User Authentication with Activation Email and Custom Notifications in Laravel 5.3
- Part 2: Login throttling, Google reCAPTCHA and front-end validations
- Part 3: Social Media login
Important Links:
You can view the live demo here and below are a couple screenshots of what we will be creating. This includes the home page, the login page and the registration page.
As you can see, the look and feel of this demo application is highly influenced by Google and Bootstrap . I also stole the Firefox “foxy” background image because I love it.
Before we begin the tutorial, its important to mention that this tutorial starts with a fresh installation of Laravel and the version that I will be using is Laravel 5.3. This version of Laravel features the new Auth controllers along with Laravel’s notification system which allows you to send various types of notifications, including emails and SMS messages. You can view my tutorial on Laravel 5.3 notifications here. We will be using both the new Auth controllers as well as the notification system in this tutorial. So let’s get started with this tutorial.
Step 1: Install a fresh copy of Laravel 5.3
The first thing to do is install a fresh copy of Laravel using composer:
composer create-project --prefer-dist laravel/laravel auth-with-activation-key
In my case, I want my project to be called “auth-with-activation-key
”. You can rename it to whatever you want. Once this is complete, you now have a fresh copy of Laravel to play with.
Modify The Environment File
Next step is to modify various parameters in the .env file. There are two important configuration sections that need to be updated: the database connection and the email provider section since we will be using Laravel’s notification system.
Setup the database connection by updating your .env file. In my case, I’m using MySQL with a database called auth-tutorial-db
[php]DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=auth-tutorial-db DB_USERNAME=my-awesome-database DB_PASSWORD=some-super-secret-password
Next, we will setup the email client for our notification system. In my case, I am using Mailtrap for doing most of my email testing but you are free to use whatever SMTP server that works for you. I use Mailtrap because all the emails that my application sends out are sent to a test inbox, which allows me to monitor things and properly test the emailing logic while I am in development mode. You can find out more at mailtrap.io.
MAIL_DRIVER=smtp MAIL_HOST=mailtrap.io MAIL_PORT=2525 MAIL_USERNAME=yourEmailUsername MAIL_PASSWORD=yourEmailPassword MAIL_ENCRYPTION=tls MAIL_FROM_ADDRESS=yourEmailSendAddress MAIL_FROM_DISPLAY_NAME=yourEmailSendDisplayName
Notice that I have also added two new fields MAIL_SEND_ADDRESS
and MAIL_SEND_DISPLAY_NAME
. These fields are the From address and From name defined in the mail.php configuratoin file. By moving them into .env.php, I have made them environment-specific. I still define a default value in mail.php just in case they are not defined in the environment file.
/* |-------------------------------------------------------------------------- | Global "From" Address |-------------------------------------------------------------------------- | | You may wish for all e-mails sent by your application to be sent from | the same address. Here, you may specify a name and address that is | used globally for all e-mails that are sent by your application. | */ 'from' => [ 'address' => env('MAIL_FROM_ADDRESS', 'default@mail.com'), 'name' => env('MAIL_FROM_DISPLAY_NAME', 'Your Display Name'), ],
Finally, I add a new environment variable called SETTINGS_SEND_ACTIVATION_EMAIL
which will determine if we should use the email activation feature or not. It is set to true by default:
SETTINGS_SEND_ACTIVATION_EMAIL=true
To make things easier in the code, I have created a configuration file called settings.php in the config folder which points to the same parameter defined in our .env file. It also sets the default value to be true. Here is the contents of the settings.php file:
return [ /* * Is email activation required */ 'send_activation_email' => env('SETTINGS_SEND_ACTIVATION_EMAIL', true) ];
We will also modify the app.php configuration file to change the name of our application. The name of the application will be ‘Laravel 5.3 Demo Auth Application’
/* |-------------------------------------------------------------------------- | Application Name |-------------------------------------------------------------------------- | | This value is the name of your application. This value is used when the | framework needs to place the application's name in a notification or | any other location as required by the application or its packages. */ 'name' => 'Laravel 5.3 Demo Auth Application',
Create the migrations
With our setting complete, we are now ready to create our migrations. We will mostly use the standard migrations that Laravel provides out of the box, but we will change them a bit so that the user has an activation key. We will also create a migration for the various activation keys. So when we are done, we will have the following 3 migrations:
- Users – defines the users table
- PasswordReset – defines the password resets table
- ActivaionKey – defines the activation keys table
We will go ahead and make some slight modifications to the users migration file. In the users migration, we will add a username
, firstname
, lastname
and activated
flag. We will also make sure that the username and email are unique. The contents of the user migration file, 2014_10_12_000000_create_users_table.php, now looks like so:
use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateUsersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); $table->string('username')->unique(); $table->string('first_name'); $table->string('last_name'); $table->string('email')->unique(); $table->string('password', 60); $table->rememberToken(); $table->boolean('activated')->default(false); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::drop('users'); } }
Next, we will modify the password reset migration that is included with Laravel. The only thing we are doing is adding an ID so that we can better track and identify the password resets. Here is the content of 2014_10_12_100000_create_password_resets_table.php migration file:
use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreatePasswordResetsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('password_resets', function (Blueprint $table) { $table->increments('id'); $table->string('email')->index(); $table->string('token')->index(); $table->timestamp('created_at')->nullable(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::drop('password_resets'); } }
Finally, we need to create the migration for the actiation keys table. This is the table that will hold the relationships between an activation key and a user’s email address. To create this migration type the following command on the console:
php artisan make:migration create_activation_keys_table
This will create a file called [*timestamp*]_create_activation_keys_table.php in the database/migrations folder. This table contains the relationship definition between user emails and activation keys. The content of this file is shown below:
use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateActivationKeysTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('activation_keys', function (Blueprint $table) { $table->increments('id'); $table->integer('user_id')->unsigned()->index(); $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); $table->string('activation_key'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('activation_keys'); } }
We are now ready to run the migrations by typing the command:
php artisan migrate
Create the models
With our migrations defined, it’s time to build the models associated to our migrations. I’m a big fan of clearly defining the structure of my data. Although Laravel has Controllers folder, Laravel, by default does not have a models folder. So, I have created my own ‘Models’ folder where I will store all my models. Here are the three models that we will be creating:
- Users
- Passwords
- ActivationKeys
First, create the Models directory, then move the prepackaged User.php
into the Models directory. We will then update the user model by modifying the namespace to App/Models to reflect the change that we just made. There are also some other minor changes but here is the full content of the user.php at the moment:
namespace App\Models; use Illuminate\Auth\Authenticatable; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; use Illuminate\Auth\Passwords\CanResetPassword; use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; use App\Notifications\PasswordResetNotification; class User extends Model implements AuthenticatableContract, CanResetPasswordContract { use Authenticatable, CanResetPassword, Notifiable; /** * The database table used by the model. * * @var string */ protected $table = 'users'; /** * The attributes that are not mass assignable. * * @var array */ protected $guarded = ['id']; /** * The attributes excluded from the model's JSON form. * * @var array */ protected $hidden = ['password', 'remember_token']; }
Note that User model extends the base model and implements the following:
AuthenticatableContract:
CanResetPasswordContract:
It also uses the following attributes:
Authenticatable
CanResetPassword
Notifiable
All of these traits are actually defined in the core Laravel code but we will cover them once we actually implement the actual authentication logic. When building the database schema for the App\Models\User
model, you should verify that your users table contains a emember_token
so that we can implement the “remember me” feature. This column will be used to store a token for users that select the “remember me” option when logging into your application. In our case, this remember_token is already done inside our migration with the the line $table->rememberToken()
.
Next we will create the ActivationKey
model. This is the model that is responsible for managing the activation keys that will be sent to users once they have registerd. For the ActivationKey
model, we need to make sure to associate it to a user using the belongsTo()
method.
You can create the ActicationKey
model using the command:
php artisan make:model Models\\ActivationKey
Here is the content of the ActivationKey
model:
namespace App\Models; use Illuminate\Database\Eloquent\Model; class ActivationKey extends Model { /** * The database table used by the model. * * @var string */ protected $table = 'activation_keys'; public function user() { return $this->belongsTo(User::class); } }
We will also go ahead and create the PasswordReset
model. For the PasswordReseset
model, there is nothing fancy to do other than assign its table so lets go ahead and do it.
Create the ActicationKey
model using the artisan command:
php artisan make:model Models\\PasswordReset
Here is the content of the PasswordReset model:
namespace App\Models; use Illuminate\Database\Eloquent\Model; class PasswordReset extends Model { /** * The database table used by the model. * * @var string */ protected $table = 'password_resets'; }
Now all our tables are created and we are ready to jump into the actual Auth logic.
Create the Auth Controllers
We are almost ready to jump into the actual authentication logic. Like I’ve said in multiple blog posts, I love Laravel! This framework is really all about rapid development and it make things so easy. Laravel provides a quick way to scaffold all of the routes and views you need for authentication using one simple command:
php artisan make:auth
So go ahead the run the command. Once you run this command, the various default Auth Controllers will be created. Laravel ships with several pre-built authentication controllers, which are located in the App\Http\Controllers\Auth namespace. Here is an overview of the various controllers. The RegisterController
handles new user registration, the LoginController
handles authentication, the ForgotPasswordController
handles e-mailing links for resetting passwords, and the ResetPasswordController
contains the logic to reset passwords. For many applications, you will not need to modify these controllers at all. And if you do, you should always override the method that you need and never modify the core of Laravel. The default logic is located inside vendor\laravel\framework\src\Illuminate
which is part of the core that you should never modify. Instead, we will create our models and controllers in the app namespace since it is removed from the core of Laravel.
The make:auth
command will also install a layout view, registration and login views, as well as routes for all authentication end-points. A HomeController
will also be generated to handle post-login requests to your application’s dashboard. You can go ahead and delete the HomeController
since we will be using our own Front and Admin controllers that we will be creating soon. Additionally a new method called Auth::routes()
is added to the routes/web.php
file. This method basically loads Laravel’s default Auth routes.
The Auth routes for Laravel 5.3 defined in the Auth::routes() method are defined as follows:
Route::group(['middleware' => ['web']], function() { // Login Routes... Route::get('login', ['as' => 'login', 'uses' => 'Auth\LoginController@showLoginForm']); Route::post('login', ['as' => 'login.post', 'uses' => 'Auth\LoginController@login']); Route::post('logout', ['as' => 'logout', 'uses' => 'Auth\LoginController@logout']); // Registration Routes... Route::get('register', ['as' => 'register', 'uses' => 'Auth\RegisterController@showRegistrationForm']); Route::post('register', ['as' => 'register.post', 'uses' => 'Auth\RegisterController@register']); // Password Reset Routes... Route::get('password/reset', ['as' => 'password.reset', 'uses' => 'Auth\ForgotPasswordController@showLinkRequestForm']); Route::post('password/email', ['as' => 'password.email', 'uses' => 'Auth\ForgotPasswordController@sendResetLinkEmail']); Route::get('password/reset/{token}', ['as' => 'password.reset.token', 'uses' => 'Auth\ResetPasswordController@showResetForm']); Route::post('password/reset', ['as' => 'password.reset.post', 'uses' => 'Auth\ResetPasswordController@reset']); });
The php artisan make:auth
command will also modify your routes/web.php
file to contain the two following lines:
Auth::routes(); Route::get('/home', 'HomeController@index');
We can remove the /home route since we have already removed HomeController and we will not be using it. We will also add three additional controllers: one for the home page, another for the admin pages, which includes the dashboard and a blank page. Before adding the actual authentication logic, we will go ahead and create a controller for the Front pages and another controller for the Admin pages. Since Laravel creates all of its Auth controllers in the Controller\Auth directory, we will follow this convention by creating a Controller\Front directory and an Controller\Admin directory. Front will contain all the content that can be accessible without being logged-in while Admin will contain the content that can only be accessed once the user is logged-in. Inside both folders, we will create a pages controller to handle the basic pages.
- Front/PagesController will contain the homepage
- Admin/PagesController will contain the dashboard and blank page
The commands to do this are:
php artisan make:controller Front\\PagesController
php artisan make:controller Admin\\PagesController
Here is the content of the Front\PagesController:
namespace App\Http\Controllers\Front; use Illuminate\Http\Request; use App\Http\Controllers\Controller; class PagesController extends Controller { public function getHome() { return view('front.pages.home'); } }
And here is the content of the Admin\PagesController:
namespace App\Http\Controllers\Admin; use Illuminate\Http\Request; use App\Http\Controllers\Controller; class PagesController extends Controller { public function getDashboard() { return view('admin.pages.dashboard'); } public function getBlank() { return view('admin.pages.blank'); } }
We will update our routes to support methods for these two controllers. Since we have not added the auth layer yet, our web route looks like so:
Route::get('/', ['as' => 'front.home', 'uses' => 'Front\PagesController@getHome']); Route::get('/admin', ['as' => 'admin.dashboard', 'uses' => 'Admin\PagesController@getDashboard']); Route::get('/admin/blank', ['as' => 'admin.blank', 'uses' => 'Admin\PagesController@getBlank']);
The entire contents of the web.php route file now looks like so:
/* |-------------------------------------------------------------------------- | Web Routes |-------------------------------------------------------------------------- | | This file is where you may define all of the routes that are handled | by your application. Just tell Laravel the URIs it should respond | to using a Closure or controller method. Build something great! | */ Route::get('/', ['as' => 'front.home', 'uses' => 'Front\PagesController@getHome']); Route::get('/admin', ['as' => 'admin.dashboard', 'uses' => 'Admin\PagesController@getDashboard']); Route::get('/admin/blank', ['as' => 'admin.blank', 'uses' => 'Admin\PagesController@getBlank']); Auth::routes();
If you try to view either of these pages, you will get errors since the views have not yet been defined. So lets go ahead and setup our views.
We will create layouts for the admin, front and auth views. And just like we did with the controllers, everything will be ordered by directory structure. I will be using a theme for each layout. Now, building a theme from scratch can be quite long and tedious. So instead, I have chosen to use freely available Bootstrap themes that I will customize for my layouts. The themes that I am using are:
- SBAdmin theme from StartBoostrap for the admin section.
- Creative theme from StartBoostrap for the front section.
- Google style login from Bootsnip for the auth section
All three themes are located in resources\views\layouts. I have tried to keep linking to local assets to a minimum. So many of the popular libraries that I use in the layouts such as jQuery and Boostrap are linked from a CDN (Content Delivery Network). Below is the contents of the views/layouts/front.blade.php
layout:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content=""> <meta name="author" content=""> <title>{{ config('app.name', 'Laravel Auth Tutorial Demo') }}</title> <!-- Bootstrap Core CSS --> <link href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.0/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css"> <!-- Custom Fonts --> <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" type="text/css"> <link href='https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800' rel='stylesheet' type='text/css'> <link href='https://fonts.googleapis.com/css?family=Merriweather:400,300,300italic,400italic,700,700italic,900,900italic' rel='stylesheet' type='text/css'> <!-- Custom CSS --> <link href="{!! asset('assets/css/front.css') !!}" rel="stylesheet"> <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script> <![endif]--> <script> window.Laravel = <?php echo json_encode([ 'csrfToken' => csrf_token(), ]); ?> </script> </head> <body id="page-top"> <nav id="mainNav" class="navbar navbar-default navbar-fixed-top"> <div class="container-fluid"> <!-- Brand and toggle get grouped for better mobile display --> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1"> <span class="sr-only">Toggle navigation</span> Menu <i class="fa fa-bars"></i> </button> <a class="navbar-brand page-scroll" href="#page-top">{{ config('app.name', 'Laravel Auth Tutorial Demo') }}</a> </div> <!-- Collect the nav links, forms, and other content for toggling --> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav navbar-right"> <li> <a class="page-scroll" href="{{ url('/login') }}">Login</a> </li> <li> <a class="page-scroll" href="{{ url('/register') }}">Register</a> </li> </ul> </div> <!-- /.navbar-collapse --> </div> <!-- /.container-fluid --> </nav> @yield('content') <!-- jQuery --> <script src="http://code.jquery.com/jquery-1.10.2.min.js"></script> <!-- Bootstrap Core JavaScript --> <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.0/js/bootstrap.min.js"></script> <!-- Plugin JavaScript --> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-easing/1.3/jquery.easing.min.js"></script> <!-- Theme JavaScript --> <script src="{!! asset('assets/js/front.js') !!}"></script> </body> </html>
The above layout template along with the view file views/front/pages/home.blade.php
produces the following screenshot:
Here is the contents of the views/layouts/admin.blade.php layout file:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content=""> <meta name="author" content=""> <title>@yield('title')</title> <meta name="description" content="@yield('description')"> <!-- Bootstrap Core CSS --> <link href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.0/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css"> <!-- Custom CSS --> <link href="{!! asset('assets/css/admin.css') !!}" rel="stylesheet"> <!-- Morris Charts CSS --> <link href="{!! asset('assets/css/plugins/morris.css') !!}" rel="stylesheet"> <!-- Custom Fonts --> <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" type="text/css"> <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script> <![endif]--> <script> window.Laravel = <?php echo json_encode([ 'csrfToken' => csrf_token(), ]); ?> </script> </head> <body> <div id="wrapper"> <!-- Navigation --> <nav class="navbar navbar-inverse navbar-fixed-top" role="navigation"> <!-- Brand and toggle get grouped for better mobile display --> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="index.html">Laravel Admin</a> </div> <!-- Top Menu Items --> <ul class="nav navbar-right top-nav"> <li class="dropdown"> @if (Auth::guest()) <a href="#"><i class="fa fa-user"></i> Unknown User</a> @else <a href="#" class="dropdown-toggle" data-toggle="dropdown"><i class="fa fa-user"></i> {{ Auth::user()->first_name }} {{ Auth::user()->last_name }} <b class="caret"></b></a> <ul class="dropdown-menu"> <li> <a href="{{ url('/admin/blank') }}"><i class="fa fa-fw fa-user"></i> Profile</a> </li> <li class="divider"></li> <li> <a href="{{ url('/logout') }}" onclick="event.preventDefault(); document.getElementById('logout-form').submit();"> <i class="fa fa-fw fa-power-off"></i> Log Out </a> <form id="logout-form" action="{{ url('/logout') }}" method="POST" style="display: none;"> {{ csrf_field() }} </form> </li> </ul> @endif </li> </ul> <!-- Sidebar Menu Items - These collapse to the responsive navigation menu on small screens --> <div class="collapse navbar-collapse navbar-ex1-collapse"> <ul class="nav navbar-nav side-nav"> <li class="active"> <a href="{{ url('/admin') }}"><i class="fa fa-fw fa-dashboard"></i> Dashboard</a> </li> <li> <a href="{{ url('/admin/blank') }}"><i class="fa fa-fw fa-bar-chart-o"></i> Charts</a> </li> <li> <a href="{{ url('/admin/blank') }}"><i class="fa fa-fw fa-table"></i> Tables</a> </li> <li> <a href="{{ url('/admin/blank') }}"><i class="fa fa-fw fa-edit"></i> Forms</a> </li> <li> <a href="{{ url('/admin/blank') }}"><i class="fa fa-fw fa-desktop"></i> Bootstrap Elements</a> </li> <li> <a href="{{ url('/admin/blank') }}"><i class="fa fa-fw fa-wrench"></i> Bootstrap Grid</a> </li> </ul> </div> <!-- /.navbar-collapse --> </nav> <div id="page-wrapper"> @yield('content') </div> <!-- /#page-wrapper --> </div> <!-- /#wrapper --> <!-- jQuery --> <script src="http://code.jquery.com/jquery-1.10.2.min.js"></script> <!-- Bootstrap Core JavaScript --> <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.0/js/bootstrap.min.js"></script> <!-- Morris Charts JavaScript --> <script src="{!! asset('assets/js/plugins/morris/raphael.min.js') !!}"></script> <script src="{!! asset('assets/js/plugins/morris/morris.min.js') !!}"></script> <script src="{!! asset('assets/js/plugins/morris/morris-data.js') !!}"></script> </body> </html>
This layout file along with views/admin/pages/dashboard.blade.php
file produces the following screenshot:
And finally here is the contents of the views/layouts/auth.blade.php
layout
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="robots" content="noindex"> <title>@yield('title')</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.0/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css"> <link rel="stylesheet" id="auth-css" href="{!! asset('assets/css/auth.css') !!}" type="text/css" media="all"> <script src="http://code.jquery.com/jquery-1.10.2.min.js"></script> <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.0/js/bootstrap.min.js"></script> </head> <body> @yield('content') </body> </html>
This layout along with the views/auth/login.blade.php
view file produces the following screenshot:
These pages should now be visible on your browser so go ahead and try them out. But you will quickly notice that it is possible for people who are not logged-in to view the admin section. That’s because we have not implemented the authentication logic. We will fix this by adding the authentication logic.
Update the Auth Configuration File
Let’s start by modifying the auth.php configuration file so that we clearly identify the model that we are using for authorization. In most cases, you will want to use the user model for authorization. But in our case, we moved the user class to the App\Models
namespace. So this has to be modified in the auth.php. Here is the updated providers array in auth.php:
/* |-------------------------------------------------------------------------- | User Providers |-------------------------------------------------------------------------- | | All authentication drivers have a user provider. This defines how the | users are actually retrieved out of your database or other storage | mechanisms used by this application to persist your user's data. | | If you have multiple user tables or models you may configure multiple | sources which represent each model / table. These sources may then | be assigned to any extra authentication guards you have defined. | | Supported: "database", "eloquent" | */ 'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => App\Models\User::class, ], ],
Update the Middleware
Next, we will update the middlewares. To understand authentication in Laravel, you need to have a good understanding of middlewares, particularly the ‘auth’ and ‘guest’ middleware. The ‘auth
’ Middleware prevents guests from entering pages that require authentication by redirecting them to the login page. The ‘guest
’ middleware checks if the current user is logged-in. If a user is logged-in, we don’t want them to have access to certain pages like the login or register page since they are already logged-in. According to Laravel’s documentation:
Middleware provide a convenient mechanism for filtering HTTP requests entering your application. For example, Laravel includes a middleware that verifies the user of your application is authenticated. If the user is not authenticated, the middleware will redirect the user to the login screen. However, if the user is authenticated, the middleware will allow the request to proceed further into the application.
The ‘guest’ middleware, along with various other middlewares are defined inside the file app/Http/kernel.php
Inside you will see a definition for ‘guest’ as so:
/** * The application's route middleware. * * These middleware may be assigned to groups or used individually. * * @var array */ protected $routeMiddleware = [ 'auth' => \Illuminate\Auth\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, ];
You will notice that the ‘guest’ definition points to a middleware called RedirectIfAuthenticated
. Each middleware in Laravel has a handle()
method which contains the core logic of the middleware. If you were to open the contents of the RedirectIfAuthenticated.php
middleware file, you will notice that it’s handle method works as such:
namespace App\Http\Middleware; use Closure; use Illuminate\Support\Facades\Auth; class RedirectIfAuthenticated { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @param string|null $guard * @return mixed */ public function handle($request, Closure $next, $guard = null) { if (Auth::guard($guard)->check()) { return redirect('/admin'); } return $next($request); } }
So, this method checks if the current user is logged-in. If they are already, then they are redirected to the /admin page. This middleware basically prevents someone who is already logged-in from having to login again. If the user is not logged-in, then they are allowed to proceed forward.
Setup the Registration Logic
The first thing that our application needs to be able to do is accept new signups. To do so, we need to modify the RegisterController, which is responsible for validating and creating new users to our application. This file is located in App/Controllers/Auth/RegisterController.php
. We will be overriding a couple of methods inside RegisterController. Remember that the base logic is inside the core of Laravel. So, whatever methods that we are writing in this file will override the ones defined inside the core of Laravel. Let’s look at the various methods that we will be overriding. The validator() method of the RegisterController contains the validation rules for new users of the application. It’s pretty self-explanatory so we will not cover anything in this function. The create()
method of the RegisterController
is responsible for creating new App\Models\User records in our database using the Eloquent ORM. We need to modify this method in order to fit our needs. There are three important things that want to do:
- we want to store the username, first_name and last_name fields in our database. These fields were all added by us.
- we want to encrypt the user’s password so that we do not store the raw password in our database
- we want to set the activated flag for email activation verification. We will set the value based on the ‘send_activation_email’ configuration value in the settings file. This settings file determines if we should send the confirmation email or simply set the user to activated automatically.
Finally, the register()
method is responsible for validating the data and registering the user. If validation passes, the user is created in our database and we call a function called queueActivationKeyNotification()
. This function is defined in a trait called ActivationKeyTrait. That is why the RegisterController
uses the trait at the beginning of the file and why we link to the file located at App\Traits\ActivationKeyTrait.php. We will cover this trait shortly, but first, here is the full code for RegisterController below:
namespace App\Http\Controllers\Auth; use App\Models\User; use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\RegistersUsers; use Illuminate\Support\Facades\Validator; use Illuminate\Http\Request; use App\Traits\ActivationKeyTrait; class RegisterController extends Controller { /* |-------------------------------------------------------------------------- | Register Controller |-------------------------------------------------------------------------- | | This controller handles the registration of new users as well as their | validation and creation. By default this controller uses a trait to | provide this functionality without requiring any additional code. | */ use RegistersUsers, ActivationKeyTrait; /** * Where to redirect users after login / registration. * * @var string */ protected $redirectTo = ''; /** * Create a new controller instance. * * @return void */ public function __construct() { $this->middleware('guest'); } /** * Get a validator for an incoming registration request. * * @param array $data * @return \Illuminate\Contracts\Validation\Validator */ protected function validator(array $data) { $validator = Validator::make($data, [ 'username' => 'required|unique:users|min:4', 'first_name' => 'required', 'last_name' => 'required', 'email' => 'required|email|unique:users', 'password' => 'required|min:6|max:20', 'password_confirmation' => 'required|same:password', ], [ 'username.required' => 'Username is required', 'username.min' => 'Username needs to have at least 6 characters', 'first_name.required' => 'First Name is required', 'last_name.required' => 'Last Name is required', 'email.required' => 'Email is required', 'email.email' => 'Email is invalid', 'password.required' => 'Password is required', 'password.min' => 'Password needs to have at least 6 characters', 'password.max' => 'Password maximum length is 20 characters', ] ); return $validator; } /** * Create a new user instance after a valid registration. * * @param array $data * @return User */ protected function create(array $data) { $user = User::create([ 'username' => $data['username'], 'first_name' => $data['first_name'], 'last_name' => $data['last_name'], 'email' => $data['email'], 'password' => bcrypt($data['password']), 'activated' => !config('settings.send_activation_email') // if we do not send the activation email, then set this flag to 1 right away ]); return $user; } public function register(Request $request) { $validator = $this->validator($request->all()); if ($validator->fails()) { $this->throwValidationException( $request, $validator ); } // create the user $user = $this->create($request->all()); // process the activation email for the user $this->queueActivationKeyNotification($user); // we do not want to login the new user return redirect('/login') ->with('message', 'We sent you an activation code. Please check your email.') ->with('status', 'success'); } }
Now let’s look at the ActivationKeyTrait
. We will go ahead and create this trait in the folder App/Traits/ ActivationKeyTrait.php
. This trait file will contain all the activation key logic. I placed the code in a trait file so that it can be reused in various controllers. In terms of where to put trait files, Laravel does not really have a convention for where you should put traits. I like to treat them like classes so I put them inside a folder called Traits inside the app directory.
Now lets look at the methods defined in the ActivationKeyTrait
. The queueActivationKeyNotification()
method initiates the activation key logic. It checks to see if we should actually execute the activation key logic or simply skip it. To do so, it checks the configuration file for activation email and makes sure that the provided email is valid. (You will see why we need to check if the email is valid later). The method validateEmail()
simply validates the input email address. The createActivationKeyAndNotify()
method is responsible for creating the activation key, assigning it to the user and firing off the notification message. Finally, processActivationKey()
is responsible for actually activating the user once the user clicks on the activation link provided in the email. The final thing that this method does is delete the activation key once the user has been activated.
Here is the full content of the trait file:
namespace App\Traits; use App\Logic\Activation\ActivationRepository; use App\Models\User; use App\Models\ActivationKey; use Illuminate\Support\Facades\Validator; use App\Notifications\ActivationKeyCreatedNotification; use App\Mails\ActivationKeyCreated; trait ActivationKeyTrait { public function queueActivationKeyNotification(User $user) { // check if we need to send an activation email to the user. If not, we simply break if ( (config('settings.send_activation_email') == false) || ($this->validateEmail($user) == false)) { return true; } $this->createActivationKeyAndNotify($user); } protected function validateEmail(User $user) { // Check that the user poses a valid email $validator = Validator::make(['email' => $user->email], ['email' => 'required|email']); if ($validator->fails()) { return false; // could not get a valid email } return true; } public function createActivationKeyAndNotify(User $user) { //if user is already activated, then there is nothing to do if ($user->activated) { return redirect()->route('front.home') ->with('message', 'This account is already activated') ->with('status', 'success'); } // check to see if we already have an activation key for this user. If so, use it. If not, create one $activationKey = activationKey::where('user_id', $user->id)->first(); if(empty($activationKey)){ // Create new Activation key for this user/email $activationKey = new ActivationKey; $activationKey->user_id = $user->id; $activationKey->activation_key = str_random(64); $activationKey->save(); } //send Activation Key notification // TODO: in the future, you may want to queue the mail since sending the mail can slow down the response $user->notify(new ActivationKeyCreatedNotification($activationKey)); } public function processActivationKey(ActivationKey $activationKey){ // get the user associated to this activation key $userToActivate = User::where('id', $activationKey->user_id) ->first(); if (empty($userToActivate)) { return redirect()->route('front.home') ->with('message', 'We could not find a user with this activation key! Please register to get a valid key') ->with('status', 'success'); } // set the user to activated $userToActivate->activated = true; $userToActivate->save(); // delete the activation key $activationKey->delete(); } }
You’ll notice that in the ActivationKeyTrait
trait, we refer to a notification called ActivationKeyCreatedNotification
. Notifications are a new feature in Laravel 5.3 that allows you to send quick notification updates through services like Slack, SMS, Email, and more. The ActivationKeyCreatedNotification
notification is used to send the activation key to the user so that they can activate their account. This notification only sends email updates and it needs to be created. So, lets go ahead and do so using artisan:
php artisan make:notification ActivationKeyCreatedNotification
This will create the new notification in app\Notifications\ActivationKeyCreatedNotification.php
. Here is the contents of this notification:
namespace App\Notifications; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Notification; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; class ActivationKeyCreatedNotification extends Notification implements ShouldQueue { use Queueable; protected $activationKey; /** * Create a new notification instance. * * SendActivationEmail constructor. * @param $activationKey */ public function __construct($activationKey) { $this->activationKey = $activationKey; } /** * Get the notification's delivery channels. * * @param mixed $notifiable * @return array */ public function via($notifiable) { return ['mail']; } /** * Get the mail representation of the notification. * * @param mixed $notifiable * @return \Illuminate\Notifications\Messages\MailMessage */ public function toMail($notifiable) { return (new MailMessage) ->subject('Your Account Activation Key') ->greeting('Hello, '.$notifiable->username) ->line('You need to activate your email before you can start using all of our services.') ->action('Activate Your Account', route('activation_key', ['activation_key' => $this->activationKey->activation_key, 'email' => $notifiable->email])) ->line('Thank you for using '. config('app.name')); } /** * Get the array representation of the notification. * * @param mixed $notifiable * @return array */ public function toArray($notifiable) { return [ // ]; } }
This notification simply emails the user (remember that notifications can also send SMS or slack messages or user any other communication medium) that they need to activate their account by clicking on the button in the email. The email contains the activation key and a clickable link for activating their account. I have customized the email message sent to the user by defining it inside the toMail()
method. Below is a sample email that a newly registered member would receive once they register:
Create the Activation Key Controller
Now the problem with the code so far is that we have not setup a controller or logic to handle what happens when the user clicks on the activation link inside the activation key email. That’s because we have not created the ActivationKeyController
yet. So let’s go ahead and create it through artisan:
php artisan make:controller Auth\\ActivationKeyController
You will notice that I place the ActivationKeyController
inside the Controllers\Auth
folder because I believe that it is part of the Auth logic. So what does the AuthController do? Let’s look at the methods in this file. The first thing that you will notice is that it uses the ‘guest’ middleware in the constructor. That is because we do not want someone who is already logged-in to have to use the activation key logic. Next, we have a validator()
method to make sure that the email address provided is valid. This is required for when we setup the logic for forgotten activation keys. The showKeyResendForm()
is the method responsible for loading the form for the ‘lost activation key’ functionality. The method activateKey()
is responsible for the actual activation logic. It checks to make sure that the activation key for the email is found and activates the account associated to the activation key. It calls the processActivationKey()
method found inside the ActivationKeyTrait. (processActivationKey()
was described a bit earlier when we covered the ActivationKeyTrait) The final method, resendKey()
handles the logic of re-sending the activation key in case the user lost the activation key. This method checks the input email and validates it. If validation passes and we can find an activation key for the provided email, we call the queueActivationKeyNotification()
method inside the ActivationKeyTrait, which will resend the activation key to the user.
As you may have noticed, the methods inside the ActivationKeyTrait
have been used in both the RegisterController
and the ActivationKeyController
. That is why this code is placed inside a trait so that it can be shared across both controllers.
Below is the full source coder for the ActivationKeyController:
namespace App\Http\Controllers\Auth; use App\Models\ActivationKey; use App\Models\User; use App\Http\Controllers\Controller; use Illuminate\Support\Facades\Auth; use App\Traits\ActivationKeyTrait; use Illuminate\Support\Facades\Validator; use Illuminate\Http\Request; class ActivationKeyController extends Controller { use ActivationKeyTrait; /** * Create a new controller instance. * * @return void */ public function __construct() { $this->middleware('guest'); } /** * Get a validator for an incoming registration request. * * @param array $data * @return \Illuminate\Contracts\Validation\Validator */ protected function validator(array $data) { $validator = Validator::make($data, [ 'email' => 'required|email', ], [ 'email.required' => 'Email is required', 'email.email' => 'Email is invalid', ] ); return $validator; } public function showKeyResendForm(){ return view('auth.resend_key'); } public function activateKey($activation_key) { // determine if the user is logged-in already if (Auth::check()) { if (auth()->user()->activated) { return redirect()->route('admin.dashboard') ->with('message', 'Your email is already activated.') ->with('status', 'success'); } } // get the activation key and chck if its valid $activationKey = ActivationKey::where('activation_key', $activation_key) ->first(); if (empty($activationKey)) { return redirect()->route('front.home') ->with('message', 'The provided activation key appears to be invalid') ->with('status', 'warning'); } // process the activation key we're received $this->processActivationKey($activationKey); // redirect to the login page after a successful activation return redirect()->route('login') ->with('message', 'You successfully activated your email! You can now login') ->with('status', 'success'); } public function resendKey(Request $request) { $validator = $this->validator($request->all()); if ($validator->fails()) { $this->throwValidationException( $request, $validator ); } $email = $request->get('email'); // get the user associated to this activation key $user = User::where('email', $email) ->first(); if (empty($user)) { return redirect()->route('activation_key_resend') ->with('message', 'We could not find this email in our system') ->with('status', 'warning'); } if ($user->activated) { return redirect()->route('login') ->with('message', 'This email address is already activated') ->with('status', 'success'); } // queue up another activation email for the user $this->queueActivationKeyNotification($user); return redirect()->route('front.home') ->with('message', 'The activation email has been re-sent.') ->with('status', 'success'); } }
Last thing that we need to do is create the view for resending lost activation keys. This is the view that is used in the showResendForm()
method. It’s a pretty simple view that simply extends that auth layout and only has one input: the user’s email address. In the end, it looks like so:
If we are able to find the email address, we will redirect the user to the homepage once we have sent out the notification and inform them that the key has been resent. If we are unable to find the email, we will send an error message to let the user know that their email was not found in our application.
Setup the Login Logic
As you already know, the users model is the model used for handing the authentication logic. By default, Laravel uses the email
field in the users table for authentication. In our case, we will be using the username
field in our sample application. So, we will have to modify the LoginController
method so that it uses the username instead of the email address. We also want the user to be redirected to /admin
URI once the user is successfully authenticated. You can customize the post-authentication redirect location by defining a redirectTo
property on the LoginController
, RegisterController
, and ResetPasswordController
That is done with this single line of code:
protected $redirectTo = '/admin';
Below is the full contents of the LoginController
namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Contracts\Auth\Guard; use Illuminate\Http\Request; class LoginController extends Controller { /* |-------------------------------------------------------------------------- | Login Controller |-------------------------------------------------------------------------- | | This controller handles authenticating users for the application and | redirecting them to your home screen. The controller uses a trait | to conveniently provide its functionality to your applications. | */ use AuthenticatesUsers; /** * Auth guard * * @var */ protected $auth; /** * Where to redirect users after login. * * @var string */ protected $redirectTo = '/'; /** * Create a new controller instance. * * LoginController constructor. * @param Guard $auth */ public function __construct(Guard $auth) { $this->middleware('guest', ['except' => 'logout']); $this->auth = $auth; } public function login(Request $request) { $username = $request->get('username'); $password = $request->get('password'); $remember = $request->get('remember'); if ($this->auth->attempt([ 'username' => $username, 'password' => $password, 'activated' => 1, ], $remember == 1 ? true : false)) { return redirect()->route('admin.dashboard'); } else { return redirect()->back() ->with('message','Incorrect username or password') ->with('status', 'danger') ->withInput(); } } }
Notice that the login()
method now checks three things:
- The username
- The password
- The activated flag
When a user is not successfully authenticated, they will be automatically redirected back to the login form. If everything checks out, we redirect the user to the admin dashboard.
Setup the route separation for admin and front areas
As it stands, we now have everything we need to register new users and have them login to our application. All we need to do is update the routes and we should be able to start testing things. Open the file routes/web.php
and we will modify it so that we split admin from front. We will also add the routes for the activation key, which will be matched to the ActivationKeyController. The contents of the routes file will now look like so:
/* |-------------------------------------------------------------------------- | Web Routes |-------------------------------------------------------------------------- | | This file is where you may define all of the routes that are handled | by your application. Just tell Laravel the URIs it should respond | to using a Closure or controller method. Build something great! | */ Route::get('/', ['as' => 'front.home', 'uses' => 'Front\PagesController@getHome']); Route::group(['namespace' => 'Admin', 'prefix' => 'admin', 'middleware' => 'auth'], function() { Route::get('/', ['as' => 'admin.dashboard', 'uses' => 'PagesController@getDashboard']); Route::get('/blank', ['as' => 'admin.blank', 'uses' => 'PagesController@getBlank']); }); // auth routes setup Auth::routes(); // registration activation routes Route::get('activation/key/{activation_key}', ['as' => 'activation_key', 'uses' => 'Auth\ActivationKeyController@activateKey']); Route::get('activation/resend', ['as' => 'activation_key_resend', 'uses' => 'Auth\ActivationKeyController@showKeyResendForm']); Route::post('activation/resend', ['as' => 'activation_key_resend.post', 'uses' => 'Auth\ActivationKeyController@resendKey']);
You will notice that we have now grouped our admin routes inside the Admin namespace and forced this group to use the auth middleware. So, now anybody trying to get to the admin section must either be logged-in or they will be redirected to the login page. We are also defining that all routes in this group must be prefixed by the ‘admin/’ URL.
That’s all you need for the activation code to work. If a user registers to the application, they will now be redirected to the login page with a message about the activation code. Here is a screenshot below:
The user will not be able to login until they have clicked on the activation link that is sent through an email. Once they click on the activation link, they will now be able to login to the system.
Then once the user provides their credentials, they will be redirected to the administrator’s dashboard. Here is a screenshot for the the admin for the robocop username.
Logging out
Logging out of the application is completely handled by Laravel’s auth logic. Once the user clicks on the logout link, the user is logged-out of the system, their session information is cleared and they are redirected to the home page.
Using the “Remember me” feature
Since we are using the base auth functionalities of Laravel, we automatically inherit the “Remember me” functionality without having to write a single line of code. It works by keeping a boolean value as the which tells the application to keep the user authenticated indefinitely, or until they manually logout. This is thanks to the fact that our users table already includes the remember_token
column, which is used by Laravel to store the “remember me” token.
Resetting passwords
Laravel handles most of the gruntwork for resetting passwords as well. The ForgotPasswordController
included with the framework already includes the logic to send the password reset link e-mails, while the ResetPasswordController
includes the logic to reset user passwords. If you go back to your migrations folder, you will notice that there is another migration file called 2014_10_12_100000_create_password_resets_table.php
. This migration will create a special table to store password resets. This table stores which user is resetting passwords in our database. All we need to do is create a form that users can use to provide their email address. If the email address is valid in our system, Laravel will add an entry in the password_reset table with the email address of the user along with a generated token. Then a notification email will then be sent to the user. This email includes a link that has the special token that was generated for the user. When the user clicks on the link, they are brought to a page where they need to enter their email and set a new password. Once this is done, Laravel also logs the user in.
The password reset uses the ‘guest’ middleware since we do not want logged-in users trying to reset their password. By default, password reset tokens expire after one hour. You may change this via the password reset expire
option in your config/auth.php
file.
Like I mentioned earlier, Laravel’s logic for this functionality is pretty much complete and there is no need to change anything. The only thing that we will be doing is updating the views so that it matches the rest of our views. There are 2 views to update:
- auth\passwords\email.blade.php which asks the user for their email
- auth\passwords\reset.blade.php which asks the user to change their password
Here is what they both look like:
If the user were to check their email, they would get an email to reset their password. Here is a sample email:
Clicking on the reset button then brings us to the reset password form like so:
Now, I personally do not like the default email that is sent for password resets. So, we will go ahead and modify this by creating a custom notification for password resets. To do so, we will modify our User model so that it uses a new notification called PasswordResetNotification
. We will also override the sendPasswordResetNotification()
method to user our new notification. So now, the full code for the user model looks like so:
namespace App\Models; use Illuminate\Auth\Authenticatable; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; use Illuminate\Auth\Passwords\CanResetPassword; use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; use App\Notifications\PasswordResetNotification; class User extends Model implements AuthenticatableContract, CanResetPasswordContract { use Authenticatable, CanResetPassword, Notifiable; /** * The database table used by the model. * * @var string */ protected $table = 'users'; /** * The attributes that are not mass assignable. * * @var array */ protected $guarded = ['id']; /** * The attributes excluded from the model's JSON form. * * @var array */ protected $hidden = ['password', 'remember_token']; /** * Overriding the exiting sendPasswordResetNotification so that I can customize it * * @var array */ public function sendPasswordResetNotification($token) { $this->notify(new PasswordResetNotification($token)); } }
Now, we need to define our PasswordResetNotification
. Here is the content of the file, which is located in the notifications folder:
namespace App\Notifications; use Illuminate\Notifications\Notification; use Illuminate\Notifications\Messages\MailMessage; class PasswordResetNotification extends Notification { /** * The password reset token. * * @var string */ public $token; /** * Create a notification instance. * * @param string $token * @return void */ public function __construct($token) { $this->token = $token; } /** * Get the notification's channels. * * @param mixed $notifiable * @return array|string */ public function via($notifiable) { return ['mail']; } /** * Get the notification message. * * @param mixed $notifiable * @return \Illuminate\Notifications\MessageBuilder */ public function toMail($notifiable) { return (new MailMessage) ->subject('Password Reset Request') ->greeting('Hello, '.$notifiable->username) ->line('You are receiving this email because we received a password reset request for your account. Click the button below to reset your password:') ->action('Reset Password', url('password/reset', $this->token).'?email='.urlencode($notifiable->email)) ->line('If you did not request a password reset, no further action is required.') ->line('Thank you for using '. config('app.name')); } }
So, as you can see, my notification message is a lot more personalized to the user. You can change the message to whatever you like. Here is what the message now looks like:
After a password is reset, the user will automatically be logged into the application and redirected to /home. You can customize the post password reset redirect location by defining a redirectTo
property on the ResetPasswordController
. In our case, we want to redirect the user to /dashboard
after a redirect:
protected $redirectTo = '/dashboard';
By default, password reset tokens expire after one hour. You may change this via the password reset expire option in your config/auth.php
file. The last thing that we need to do is send a password reset confirmation email once the user has reset their password. I recently had to change my password on one of the social networks and I noticed that this service also sends you an email when your email address has been reset. I think that this is good practice so we will go ahead and do this with our application. We will simply modify the ResetPasswordController
so that we override the method sendResetResponse()
and set our redirect after login link to /admin
. In case yo are wondering, sendResetResponse()
is the method responsible for redirecting the user after a successful password reset. I am updating his function to also notify the user that their password has been reset. I am calling $user = Auth::user()
to get the user since the user Here is already authenticated into the application. As a sidenote, If I did not want the user to be automatically logged-in after a password reset, this is where I would do it too. The full contents of ResetPasswordController:
namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\ResetsPasswords; use App\Notifications\PasswordResetConfirmationNotification; use Illuminate\Support\Facades\Auth; class ResetPasswordController extends Controller { /* |-------------------------------------------------------------------------- | Password Reset Controller |-------------------------------------------------------------------------- | | This controller is responsible for handling password reset requests | and uses a simple trait to include this behavior. You're free to | explore this trait and override any methods you wish to tweak. | */ use ResetsPasswords; /** * Where to redirect users after login. * * @var string */ protected $redirectTo = '/admin'; /** * Create a new controller instance. * * @return void */ public function __construct() { $this->middleware('guest'); } /** * Get the response for a successful password reset. * * @param string $response * @return \Illuminate\Http\Response */ protected function sendResetResponse($response) { // Get the currently authenticated user... $user = Auth::user(); //send Activation Key notification // TODO: in the future, you may want to queue the mail since sending the mail can slow down the response $user->notify(new PasswordResetConfirmationNotification()); return redirect($this->redirectPath()) ->with('message', 'Your password has been successfully updated.') ->with('status', 'success'); } }
Below is a screenshot of the success message and email after a successful password reset.
Handling people who forget their username
So we have handled the case of users that forget their password. But what about users who forget their username? Rember that, in order to login to our system, you need to give us your username and password. So lets go ahead and setup a controller for users that forget their username. Once again, I consider this as part of the authentication process so I have placed the newly created items in the auth subfolder. Let’s start with the controller. Lets create it using artisan
php artisan make:controller Auth\\ForgotUsernameController
And here is the content of the controller:
namespace App\Http\Controllers\Auth; use Illuminate\Http\Request; use App\Models\User; use App\Http\Controllers\Controller; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Validator; use App\Notifications\UsernameReminderNotification; class ForgotUsernameController extends Controller { // /** * Create a new controller instance. * * @return void */ public function __construct() { $this->middleware('guest'); } /** * Get a validator for an incoming registration request. * * @param array $data * @return \Illuminate\Contracts\Validation\Validator */ protected function validator(array $data) { $validator = Validator::make($data, [ 'email' => 'required|email', ], [ 'email.required' => 'Email is required', 'email.email' => 'Email is invalid', ] ); return $validator; } public function showForgotUsernameForm(){ return view('auth.username'); } public function sendUserameReminder(Request $request) { $validator = $this->validator($request->all()); if ($validator->fails()) { $this->throwValidationException( $request, $validator ); } $email = $request->get('email'); // get the user associated to this activation key $user = User::where('email', $email) ->first(); if (empty($user)) { return redirect()->route('username_reminder') ->with('message', 'We could not find this email in our system') ->with('status', 'warning'); } //send Activation Key notification // TODO: in the future, you may want to queue the mail since sending the mail can slow down the response $user->notify(new UsernameReminderNotification()); return redirect()->route('front.home') ->with('message', 'Your username has been sent to your email address') ->with('status', 'success'); } }
Lets look at the methods inside this controller and understand what they do… The constructor defines the middleware that its using. Once again, this controller uses that guest
middleware since we want to make sure that only people that are not logged-in can access this controller. The validator()
method just defines the volition rules for the email address provided by the user. The method showForgotUsernameForm()
displays the actual form for the user to enter their email address. sendUserameReminder()
is where the magic happens. It validates the user’s email address and if it’s all good, it sends a UsernameReminderNotification
notification message to the user.
We need to add these new methods inside our routes file. By adding the following lines:
// forgot_username Route::get('username/reminder', ['as' => 'username_reminder', 'uses' => 'Auth\ForgotUsernameController@showForgotUsernameForm']); Route::post('username/reminder', ['as' => 'username_reminder.post', 'uses' => 'Auth\ForgotUsernameController@sendUserameReminder']);
So, the next obvious question is what’s in the UsernameReminderNotification
. Well here is its content below:
namespace App\Notifications; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Notification; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; class UsernameReminderNotification extends Notification implements ShouldQueue { use Queueable; /** * Create a new notification instance. * * SendActivationEmail constructor. * @param $token */ public function __construct() { // nothing special to do } /** * Get the notification's delivery channels. * * @param mixed $notifiable * @return array */ public function via($notifiable) { return ['mail']; } /** * Get the mail representation of the notification. * * @param mixed $notifiable * @return \Illuminate\Notifications\Messages\MailMessage */ public function toMail($notifiable) { return (new MailMessage) ->subject('Your '.config('app.name').' Username') ->greeting('Hello, '.$notifiable->username) ->line('You recently sent us a request for your username on our app. Your username is '.$notifiable->username) ->action('Login as '.$notifiable->username, route('login')) ->line('Thank you for using '. config('app.name')); } /** * Get the array representation of the notification. * * @param mixed $notifiable * @return array */ public function toArray($notifiable) { return [ // ]; } }
Finally, we will update the views so that we can display the forgot username page. The view produces the following output:
And here is a sample email that will be sent by the notification system:
And there you have it. You now have a complete authentication application in Laravel 5.3 with Activation key emails as well as forgot password and forgot username emails. As mentioned earlier, you can play with the demo here and you can download the entire code here or at Github.
Hello mate, i use your tutorial step by step but in login page got error: Parse error: syntax error, unexpected ‘,’ login.blade.php in line 479
Can u help? Thanks!
Hello Ivaylo. can you please give a little more details about the error. I will try to fix it as soon as i have time. Did you download the zip file? Or is this from the Github repo?
Hi Sir! I follow your tutorial but found thing which missing. So system sends activation e-mail to user but if he’s not activate his account and try to login, system says: “Incorrect username or password”
Need edit this to something like: “This account is not activated. Please activate!”
And when enter wrong username or password ->”Incorrect username or password”
Hey Ivaylo. All you need to do is modify the LoginController to add this logic. In the login() method, you can change the code so that you check for the activated flag and then block the user from logging-in if their account has not been activated yet.
Thanks for quick answer. So don’t know how check it, can u help me with content of code ? Thanks.
Here is my logic but error appear: What’s wrong?
if (auth()->user()->activated == ‘0’){
$this->logout();
return back()->with(‘warning’,”Your account is not activated. Please activate it!”);
}
return redirect()->to(‘admin’);
}else{
return back()->with(‘error’,’your username and password are wrong.’);
}
Hi Ivayli,
You are on the right path. The only thing is that you need to do the verification for the activated flag after you call the attempt() method. Here is what the code should look like:
Another way that you could go about doing this without modifying the login method is by creating a handler that listens to the login event. Once the event is fired, you can catch it do the active flag and logout logic in the handler. You can find a good example here: http://stackoverflow.com/questions/22460066/laravel-last-login-date-and-time-timestamp#answer-34521860
hello sir, i follow all of the step and i got this error
FatalErrorException in User.php line 13: Class App\Models\User contains 8 abstract methods and must therefore be declared abstract or implement the remaining methods (Illuminate\Contracts\Auth\Authenticatable::getAuthIdentifierName, Illuminate\Contracts\Auth\Authenticatable::getAuthIdentifier, Illuminate\Contracts\Auth\Authenticatable::getAuthPassword, …)
Hey Mifty. I have done until registration part but the problem is I am using xampp. So when I register I dont get any email. Is it because of I am using localhost? Thanks a lot for this awesome tutorial
Hi Saad,
If you want to receive your emails, then use mailtrap, as I mentioned in the tutorial. Mailtrap is a free mail service that is perfect for web development since it allows you to send test emails from your application. I also use xampp for my development and mailtrap works well with xampp
Thanks a lot for the reply mifty. Whenever I am registering it gives me this following error 🙁
Swift_TransportException in StreamBuffer.php line 269:
Connection could not be established with host mailtrap.io [A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.
#10060]
I have used mailtrap username and password in the config but still it is not working. Can you please help me?
Hi, it gives this error when your settings is not correct.
Hi Ivaylo, thanks for the reply. Can you suggest me something? Which setting are you indicating? The env file?
Hi, your env file looks like:
MAIL_DRIVER=smtp
MAIL_HOST=mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=your@email
MAIL_PASSWORD=your@email password
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=your send address
MAIL_FROM_DISPLAY_NAME=your display name
SETTINGS_SEND_ACTIVATION_EMAIL=true
In /app/config/mail.php
‘host’ => env(‘MAIL_HOST’, ‘mailtrap.io’),
‘port’ => env(‘MAIL_PORT’, 2525)
Thanks ivaylo! I wanted to know the username and password. Is it the one that I used when I created my mailtrap account?
Yes.
Still same error 🙁
Hi Saad,
Sorry to hear that the issue is still happening to you. If you are sure that your username and password are good, then I suspect that you might not be using the right encryption of port. Are you sure that you have the right SMTP port and security encryption in your env file?
Hello sir it learn your code but i don`t understand what is notification and how to got PasswordResetNotification
Nice Tutorial. Thank you so much.
Hi,
In the Trait ActivationKeyTrait.php, you have these two lines:
use App\Logic\Activation\ActivationRepository;
use App\Mails\ActivationKeyCreated;
I didn’t find these directories in your source and you don’t talk about them.
I was wondering about this too! I have not finished it yet so don’t know if/how this might affect it (I am very new to Laravel so if that sounds silly please don’t judge).
Have you managed to finish the tutorial and test it?
I have played around with the demo and it seems to work just fine but does following the steps exactly replicate the demo?
Thanks..
Hi kabed,
This is exactly why you should always cleanup your code before making it public 🙂
Both files are not needed and are actually test files that ended up deleting but that I never cleaned up in the code. I often use repositories in my Laravel application but did not use one in this tutorial. I also did not need the Mails folder since I am using Laravel’s notifications system to send out emails
Hi
thanks for sharing the code, it tells me that Class\App\User not found, please explain why even if I place the file back to App
chief you are damn good, this is excelent tutorial :), i got it working
Hi
thanks for the great tutorial, but I landed in one issue here, when I created a link of blank, it doesnt access the page, it says notfoundhttpexception, I have been trying to fix the middleware issue which I am not really familiar with, please help
Excellent Tutorial,
But want to point out a couple of things, you did not explain how you created the PasswordResetConfermationNotification or the UsernameReminderNotification, I had to download your source code and copy them from there , just thought you might want to update the tutorial to include those ! otherwise really good, looking forward going through the google recapcha tutorial.
Thanks for the find DWisenbaugh. I’ll update the tutorial to include this information. I totally forgot to add that part in the tutorial
views/front/pages/home.blade.php
Where are the files for this sir?
this is the error -_-
View [front.pages.home] not found.
Very cool!! Thanks Mifty!!
A quick question — where is the code for the throwValidationException method you call in RegisterController.php ?
And please don’t say “Batman”.
Hi David,
I don’t think that I every actually defined this error. That is why the file does not exist in the repo. Sorry, it’s been a while since I looked at this tutorial.
My error
helpme
thank you
LoL! You are welcome
Hi thanks for the tuto. I can not find de link for download?
Hi. thanks for the tuto it is too good. I reached a problem with reset password :
UnexpectedValueException
User must implement CanResetPassword interface.
Please help