User Activation via email and Laravel Middleware for Activated Users

Intro

In previous tutorials of Building Laravel 5.3 app with Multi-authentication and Social logins series we developed simple Laravel app with email registration and authentication service. That application didn't posses any activation mechanism, and because that is usual requirement I'll guide you through development process of that feature.

First thing after successful registration is to send activation email to the user with activation link.

That email will posses activation link with unique token, so from Laravel side we will first need to create activation record for that user with unique token.

We need new table for keeping activation records and dedicated model. 

After sending email new users will be able to activate their account by visiting provided link, so new route is required with dedicated Controller.

It happens often that user never receives activation email and Resend activation email feature is crucial. In auth middleware we will check is user activated and if not application will display warning message above navigation bar with Resend link.

New activation record will be added to activation table with each new Activation request, so if user clicks Resend 10 times, then activation table will posses 11 records for that user including original one (sent after registration immediately). It is good to have some sort of cleaning mechanism for old activation, so we'll create scheduled command which will delete activation records older then 72 hours.

Activation is usually needed in web apps were users are able to do some sort of privileged action, like writing post or uploading files. That's why we will create dedicated middleware which will be able to protect entire group of routes. If user who is not activated tries to access protected routes, system will redirect him to error page. 

Sometimes administrators want to disable activation feature, so all new users are activated by default, we will add this feature. Only with changing one key in .env file system administrator will be able to turn on/off activation service.

This plan can be summarized with following list:

  1. Creating Activation table and Eloquent model
  2. Logic for creating new Activation record
  3. Activation email via Laravel Notifications
  4. Adding new route for activating user
  5. Creating Activation controller with method for activating user
  6. Sending Activation email after successful registration
  7. Modifying Authenticate Middleware to check is user activated and display message if it's not
  8. Adding new route for resending activation email
  9. Creating method for resending activation email in Activation controller
  10. Creating Middleware for activated users
  11. Adding new middleware to routes file and creating protected routes
  12. Scheduled command for deleting expired activation records

I think everything important is covered here, so lets start.

Creating Activation table and Eloquent model

Activations table will hold active activation records for new users. It will posses user_id and token columns.

php artisan make:migration create-activations-table --create=activations

 This will create new migration file, lets add columns there:

<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateActivationsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('activations', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('user_id')->unsigned()->index();
            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
            $table->string('token');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('activations');
    }
}

Dedicated Eloquent model is not required, you can use Query Builder instead of a model, but I prefer Eloquent.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Activation extends Model
{

    public function user()
    {
        return $this->belongsTo(User::class);
    }

}

As you can see it posses relation to User model.

Logic for creating new Activation record

Creating activation record is separate action from registering new user so we will split that logic into new class, lets call it ActivationRepository. As you can see I am keeping that new file in app/Logic/Activation folder.

<?php

namespace App\Logic\Activation;

use App\Models\Activation;
use App\Models\User;

class ActivationRepository
{

    public function createTokenAndSendEmail(User $user)
    {

        if ($user->activated) { //if user changed activated email to new one

            $user->update([
                'activated' => false
            ]);

        }

        // Create new Activation record for this user/email
        $activation = new Activation;
        $activation->user_id = $user->id;
        $activation->token = str_random(64);
        $activation->save();

        // Send activation email notification


    }

}

Method createTokenAndSendEmail accepts User model instance and first it checks is user already activated, if yes then it flips activated flag to false. I've done it this way because users are sometimes able to change their email address and if that is the case then we need to invalidate previous activation. You can omit this part if you don't plan to provide email changing feature, or if that is not important for you.

After that I am creating Activation record with user_id and random token. This token will be used in activation emails, as way to distinguish different users.

As you can see later we'll add email part to this method, first we have to create new email notification.

Activation email via Laravel Notifications

From version 5.3 Laravel posses support for sending notifications to different channels, in pretty awesome way. Checkout Laravel official Notifications documentation if you are curious how it works.

Here we will create new notification for sending activation emails.

php artisan make:notification SendActivationEmail

This command will create new file in app/Notifications folder.

<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;

class SendActivationEmail extends Notification implements ShouldQueue
{
    use Queueable;

    protected $token;

    /**
     * Create a new notification instance.
     *
     * SendActivationEmail constructor.
     * @param $token
     */
    public function __construct($token)
    {
        $this->token = $token;
    }

    /**
     * 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('Activation email')
                    ->greeting('Hello!')
                    ->line('You need to activate your email before you can start using all of our services.')
                    ->action('Activate Email', route('authenticated.activate', ['token' => $this->token]))
                    ->line('Thank you for using our application!');
    }

    /**
     * Get the array representation of the notification.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function toArray($notifiable)
    {
        return [
            //
        ];
    }
}

I added token property to this new notification class, cause I plan to pass it via notification constructor to email. 

In toMail method you can see that I am using that token to create activation route in the email.

Adding new route for activating user

This route will accept one required parameter token, and I'll add it into group of routes behind all roles middleware which we created in the first tutorial.

Route::group(['middleware' => 'auth:all'], function()
{
    $a = 'authenticated.';
    Route::get('/logout', ['as' => $a . 'logout', 'uses' => 'Auth\LoginController@logout']);
    Route::get('/activate/{token}', ['as' => $a . 'activate', 'uses' => 'ActivateController@activate']);
    
});

Creating Activation Controller with method for activating user

Activation controller will hold 2 methods one for activating user and other for resending activation email. For now lets create first one only:

<?php

namespace App\Http\Controllers;

use App\Models\Activation;

class ActivateController extends Controller
{

    public function activate($token)
    {
        if (auth()->user()->activated) {

            return redirect()->route('public.home')
                ->with('status', 'success')
                ->with('message', 'Your email is already activated.');
        }

        $activation = Activation::where('token', $token)
            ->where('user_id', auth()->user()->id)
            ->first();

        if (empty($activation)) {

            return redirect()->route('public.home')
                ->with('status', 'wrong')
                ->with('message', 'No such token in the database!');

        }

        auth()->user()->activated = true;
        auth()->user()->save();

        $activation->delete();

        session()->forget('above-navbar-message');

        return redirect()->route('public.home')
            ->with('status', 'success')
            ->with('message', 'You successfully activated your email!');

    }

}

This method is doing couple of actions:

  • Checking is user activated
  • Check is provided token valid
  • If it's valid user is activated
  • Activation record is removed from activations table
  • Session value which holds information is user activated is removed (Used for displaying Resend above navbar message)

Sending Activation email after successful registration

In this application there are 2 scenarios how activation email could be sent: after registration, after resend action; so to be able to reuse same logic I will create a trait. I will name new trait ActivationTrait and place it inside app/Traits folder.

<?php

namespace App\Traits;

use App\Logic\Activation\ActivationRepository;
use App\Models\User;
use Illuminate\Support\Facades\Validator;

trait ActivationTrait
{

    public function initiateEmailActivation(User $user)
    {

        if ( !config('settings.activation')  || !$this->validateEmail($user)) {

            return true;

        }

        $activationRepostory = new ActivationRepository();

        $activationRepostory->createTokenAndSendEmail($user);

    }

    protected function validateEmail(User $user)
    {

        // Check does email posses valid format, cause if it's social account without
        // email, it'll have value of missingxxxxxxxxxx
        $validator = Validator::make(['email' => $user->email], ['email' => 'required|email']);

        if ($validator->fails()) {
            return false; // Stopping job execution, if it return false it will break entire application
        }

        return true;

    }

}

Method initiateEmailActivation accepts User model instance, it checks is activation enabled in the configuration file and is user email in valid format. For checking email format I am using validateEmail method, which is doing basic Laravel validation on email field. I am doing this cause I want to be sure that we are sending email to real email address (even if I know that validation is already completed on registration,.. but what about email update?).

Later I am reusing method createTokenAndSendEmail from ActivationRepository,.. some reusable code :-)

Now the only thing left is to call initiateEmailActivation method from RegisterController:

// RegisterController.php
// ...

    /**
     * Create a new user instance after a valid registration.
     *
     * @param  array  $data
     * @return User
     */
    protected function create(array $data)
    {

        $user =  User::create([
            'first_name' => $data['first_name'],
            'last_name' => $data['last_name'],
            'email' => $data['email'],
            'password' => bcrypt($data['password']),
            'token' => str_random(64),
            'activated' => !config('settings.activation')
        ]);

        $role = Role::whereName('user')->first();
        $user->assignRole($role);

        $this->initiateEmailActivation($user);

        return $user;

    }

Don't forget to include use statement for ActivationTrait.

Now after user is successfully registered, system will send activation email. Lets test it:

Activation Email

Activation Email

I received an email, but I had to wait a couple of seconds cause I am not using queue for emails. I'll fix that later.

Modifying Authenticate Middleware to check is user activated and display message if it's not

Idea is to display reminder above navigation bar that user is not activated. Something like this:

Above Navbar Message

Above Navbar Message

I will create new partial file above-navbar-alert with following code:

@if(session()->has('above-navbar-message') && auth()->check())
    <div class="alert alert-info" role="alert" style="margin-bottom:0px;background-color:#000;border-color:#000;color:#fff;">
        <button type="button" class="close" data-dismiss="alert" style="color:#fff;">×</button>
        {!! session()->get('above-navbar-message') !!}
    </div>
@endif

And I'll include it in main layout after opening header tag:

// main.blade.php
// ...

<!--Navigation-->
<header>

@include('partials.above-navbar-alert')

<!--Navbar-->
    <nav class="navbar navbar-dark scrolling-navbar mdb-gradient">

// ...

From partial code you can see that it checks for existance of session variable above-navbar-message and it checks is user authenticated. So message won't be displayed to guest users, cause this is main layout file.

If both terms are satisfied message is printed in the alert.

Now we need middleware logic which will add this session variable.

I will use Authenticate middleware from previous tutorial and add small snippet there:

// Authenticate.php
// ...

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param $role
     * @return mixed
     */
    public function handle($request, Closure $next, $role)
    {
        if(!$this->auth->check())
        {
            return redirect()->to('/login')
                ->with('status', 'success')
                ->with('message', 'Please login.');
        }

        if (config('settings.activation')) {
            if ($this->auth->user()->activated == false) {
                session()->put('above-navbar-message', 'Please activate your email. <a href="'. route('authenticated.activation-resend') .'">Resend</a> activation email.');
            } else {
                session()->forget('above-navbar-message');
            }
        }

        if($role == 'all')
        {
            return $next($request);
        }
        if( $this->auth->guest() || !$this->auth->user()->hasRole($role))
        {
            abort(403);
        }
        return $next($request);
    }
}

New if-else statement is pretty self-explanatory, if activation is enabled -> check is user activated-> if not activated, add session variable -> else delete session variable.

I introduced here new resend route in this middleware, so lets create it.

Adding new route for resending activation email

I will ad this route under authenticated.activate route, I added before:

    Route::get('/activate', ['as' => $a . 'activation-resend', 'uses' => 'ActivateController@resend']);

Creating method for resending activation email in ActivateController

As I mentioned in paragraph Sending Activation Email we will reuse same logic using ActivationTrait.

// ActivateController.php
// ...

    public function resend()
    {
        $this->initiateEmailActivation(auth()->user());

        return redirect()->route('public.home')
            ->with('status', 'success')
            ->with('message', 'Activation email sent.');
    }

// ...

Creating Middleware for activated users

This new piece of middleware is doing simple check, I could mix this check in main auth middleware, but that would be one very ugly middleware. 

php artisan make:middleware CheckIsUserActivated

Lets modify this new file.

<?php

namespace App\Http\Middleware;

use Closure;

class CheckIsUserActivated
{

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if ( (auth()->user()->activated == false) && config('settings.activation')) {

            return redirect()->route('not-activated');

        }

        return $next($request);
    }
}

If user is not activated and activation is enabled, he's redirected to some page which displays error message. You can do whatever you want here, I am redirecting user to the same page as 404 error cause I am lazy.

   Route::get('not-activated', ['as' => 'not-activated', 'uses' => function () {
        return view('errors.not-activated');
    }]);

Now we will invoke this new middleware in Http Kernel:

// Kernel.php
// ...

    protected $routeMiddleware = [
        'auth' => 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,
        'activated' => CheckIsUserActivated::class
    ];

// ...

Adding new middleware to routes file and creating protected routes

Cause I invoked new middleware in Kernel I can use it in my route groups in the same way as auth middleware.

Route::group(['prefix' => 'user', 'middleware' => 'auth:user'], function()
{
    $a = 'user.';
    Route::get('/', ['as' => $a . 'home', 'uses' => 'UserController@getHome']);

    Route::group(['middleware' => 'activated'], function ()
    {
        $m = 'activated.';
        Route::get('protected', ['as' => $m . 'protected', 'uses' => 'UserController@getProtected']);
    });

});

Now you have place where you can place all routes which require activated users. 

Scheduled command for deleting expired activation records

This one is easy, we need to check are there any activation records older than 72 hours, if they exist we'll delete them.

php artisan make:command DeleteExpiredActivations

We will inject ActivationRepository instance through constructor of this command, and later we'll add new method to the repository for deleting expired activations.

<?php

namespace App\Console\Commands;

use App\Logic\Activation\ActivationRepository;
use Illuminate\Console\Command;

class DeleteExpiredActivations extends Command
{
    protected $activationRepository;

    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'codingo:delete-expired-activations';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Delete activation records older than 72 hours.';

    /**
     * Create a new command instance.
     *
     * DeleteExpiredActivations constructor.
     * @param ActivationRepository $activationRepository
     */
    public function __construct(ActivationRepository $activationRepository)
    {
        parent::__construct();
        $this->activationRepository = $activationRepository;
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $this->activationRepository->deleteExpiredActivations();
    }
}

I named new method deleteExpiredActivations, lets create it:

// ActivationRepository.php
// ...

    public function deleteExpiredActivations()
    {

        Activation::where('created_at', '<=', Carbon::now()->subHours(72))->delete();

    }

// ...

Now only thing left is to initiate it in Kernel file and create schedule.

<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    /**
     * The Artisan commands provided by your application.
     *
     * @var array
     */
    protected $commands = [
        \App\Console\Commands\DeleteExpiredActivations::class,
    ];

    /**
     * Define the application's command schedule.
     *
     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {
        $schedule->command('codingo:delete-expired-activations')
                 ->daily();
    }
}

If you are new to Laravel Task Scheduling it is important to mention that you have to create main Cron job entry for your application on your web server. That cron will call schedule:run function each minute.

* * * * * php /path/to/artisan schedule:run >> /dev/null 2>&1

For testing purposes you can manually call this command:

php artisan codingo:delete-expired-activations

Limit number of activation attempts

Always when I am developing I am thinking of ways how someone can misuse my applications and how can I prevent that from happening. 

In this application users can request resending of activation emails, so if someone clicks 10 times that links, should system send them? What if they create console snippet which clicks the same link 1000 times? :) 

Believe me I've seen many similar attempts during my career and I would advise you to make steps to prevent it. If you are paying for sending emails to AWS, someone could make your bill huge, just by doing this. Or it can prevent regular users from receiving any emails from your application.

In our application this is pretty easy to do, we need to add one simple check in the ActivationRepository.

// ActivationRepository.php
// ...

    public function createTokenAndSendEmail(User $user)
    {
        // Limit number of activation attempts to 3 in 24 hours window
        $activations = Activation::where('user_id', $user->id)
            ->where('created_at', '>=', Carbon::now()->subHours(24))
            ->count();

        if ($activations >= 3) {
            return true;
        }

        // ...

// ...

This piece of code is checking are there more than 3 activation attempts in last 24 hours for this user, if it's true it returns without sending actual email or inserting new activation record.

You can return some sort of notification to user different from default "Activation email sent." notification.

Closing

How do you like my implementation? Do you have some improvement or do you have different way of activating users? Comment bellow so we can make this tutorial great together.

Lessons of this Series

Online computer science courses to jumpstart your future.
WP Engine Managed WordPress Hosting

Trending

Newsletter

Subscribe to our newsletter for good news, sent out every month.

Tags