+977-9866804165/1-4782129

In this tutorial I will create Laravel application with email authentication, but also I will use Laravel Socialite for Facebook and Twitter logins. Once when I configure everything, app will also be able to use many other social platforms for sign in process. Entire list of social providers ishere. Some of them are PaypalRedditLinkedinTumblrYoutube and Google.

I will start from new Laravel installation, for detailed installation steps check here. After installation I will copy .env.example file to .env and insert database credentials there.

While I am developing web projects I like to configure fake .dev domains. My local development machine is running Ubuntu 14.04 and Apache2 web-server, so I configured virtual hosts and modified /etc/hosts file to point tuts1.dev to localhost. You can do this a number of ways, also you don't need to use fake domains. I pointed out this, cause throughout tutorial you will be seeing that domain.

Development schedule

It is always good idea to create some kind of schedule or plan, before you actually start coding. So I like to write down simple steps and then complete them one by one, until entire app is completed.

For now this app will have home page, login/register pages with forms for email authentication and Facebook and Twitter buttons for social login. Usually every app needs to have at least 2 user roles, for administrator and for ordinary users, so I will code basic user-role logic. Users will be able to reset their passwords, so app will send some emails. I will code system in such a way that adding new social providers like Github is going to be trivial and short process (under 20 seconds).

This schedule is not strict it is more like a guide. I always like to complete frontend first so I can enjoy building logic and backend later.

Creating views

When it comes to HTML, CSS and other frontend stuff I like to use Bootstrap framework. I will use example projects from their site, so lets go to Bootstrap site  and download the code. I have extracted Bootstrap to public/assets/plugins/bootstrap

Layout

Lets grab 2 examples from Bootstrap Examples : Static top navbar and Sign-in page. We will do some quick modifications of these 2 projects and create all needed pages. This first example code will make our main 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">
    <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
    <meta name="description" content="">
    <meta name="author" content="">
    <link rel="icon" href="../../favicon.ico">

    <title>Static Top Navbar Example for Bootstrap</title>

    <!-- Bootstrap core CSS -->
    <link href="../../dist/css/bootstrap.min.css" rel="stylesheet">

    <!-- Custom styles for this template -->
    <link href="navbar-static-top.css" rel="stylesheet">

    <!-- Just for debugging purposes. Don't actually copy these 2 lines! -->
    <!--[if lt IE 9]><script src="../../assets/js/ie8-responsive-file-warning.js"></script><![endif]-->
    <script src="../../assets/js/ie-emulation-modes-warning.js"></script>

    <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
    <!--[if lt IE 9]>
      <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
      <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
    <![endif]-->
  </head>

  <body>

    <!-- Static navbar -->
    <nav class="navbar navbar-default navbar-static-top">
      <div class="container">
        <div class="navbar-header">
          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
            <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="#">Project name</a>
        </div>
        <div id="navbar" class="navbar-collapse collapse">
          <ul class="nav navbar-nav">
            <li class="active"><a href="#">Home</a></li>
            <li><a href="#about">About</a></li>
            <li><a href="#contact">Contact</a></li>
            <li class="dropdown">
              <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a>
              <ul class="dropdown-menu">
                <li><a href="#">Action</a></li>
                <li><a href="#">Another action</a></li>
                <li><a href="#">Something else here</a></li>
                <li role="separator" class="divider"></li>
                <li class="dropdown-header">Nav header</li>
                <li><a href="#">Separated link</a></li>
                <li><a href="#">One more separated link</a></li>
              </ul>
            </li>
          </ul>
          <ul class="nav navbar-nav navbar-right">
            <li><a href="../navbar/">Default</a></li>
            <li class="active"><a href="./">Static top <span class="sr-only">(current)</span></a></li>
            <li><a href="../navbar-fixed-top/">Fixed top</a></li>
          </ul>
        </div><!--/.nav-collapse -->
      </div>
    </nav>


    <div class="container">

      <!-- Main component for a primary marketing message or call to action -->
      <div class="jumbotron">
        <h1>Navbar example</h1>
        <p>This example is a quick exercise to illustrate how the default, static and fixed to top navbar work. It includes the responsive CSS and HTML, so it also adapts to your viewport and device.</p>
        <p>To see the difference between static and fixed top navbars, just scroll.</p>
        <p>
          <a class="btn btn-lg btn-primary" href="../../components/#navbar" role="button">View navbar docs &raquo;</a>
        </p>
      </div>

    </div> <!-- /container -->


    <!-- Bootstrap core JavaScript
    ================================================== -->
    <!-- Placed at the end of the document so the pages load faster -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
    <script src="../../dist/js/bootstrap.min.js"></script>
    <!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
    <script src="../../assets/js/ie10-viewport-bug-workaround.js"></script>
  </body>
</html>

I will copy this code into views/layouts/main.blade.php this will be layout file. In earlier versions of Laravel, framework was shipped with built-in HTML helpers, but not any more, so I need to import them. In composer.json file I will add html helper require statement and Socialite package cause I will use that later:


.....
    "require": {
        "php": ">=5.5.9",
        "laravel/framework": "5.1.*",
        "illuminate/html": "5.*",
        "laravel/socialite": "^2.0"
    },

After adding new packages to composer.json I need to update the system with: composer update

You can also add these 2 packages without manually modifying composer.json file or updating all dependencies with composer update For illuminate/html package composer require illuminate/html and for Socialite composer require laravel/socialite. This option is much faster than updating all the dependencies.

Now I will rewrite this page in Blade syntax, and add few sections like head and content. In head section later I will be able to include custom css and js files for each page and content section is for HTML content of certain page.


<!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">
    <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
    <meta name="description" content="">
    <meta name="author" content="">
    <link rel="icon" href="../../favicon.ico">

    <title>Laravel Social and Email Authentication</title>

    <!-- Bootstrap core CSS -->
    {!! HTML::style('/assets/plugins/bootstrap/css/bootstrap.min.css') !!}

    <!-- Custom styles for this template -->
    {!! HTML::style('/assets/css/navbar-static-top.css') !!}

    <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
    <!--[if lt IE 9]>
    <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
    <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
    <![endif]-->

    @yield('head')

</head>

<body>

<!-- Static navbar -->
<nav class="navbar navbar-default navbar-static-top">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
                <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="#">Social Authentication</a>
        </div>
        <div id="navbar" class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li><a href="#">Home</a></li>
            </ul>
            <ul class="nav navbar-nav navbar-right">
                <li><a href="#">Login</a></li>
                <li><a href="#">Register</a></li>
            </ul>
        </div><!--/.nav-collapse -->
    </div>
</nav>


<div class="container">

    @yield('content')

</div> <!-- /container -->


<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
{!! HTML::script('/assets/plugins/bootstrap/js/bootstrap.min.js') !!}
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
{!! HTML::script('/assets/js/ie10-viewport-bug-workaround.js') !!}
</body>
</html>

In this layout file you could see that I am importing style sheets and js files with trailing slash at the beginning. I can use this cause remember that my site is using dummy domain, but if I was serving this site from localhost/some-folder/my-laravel-project/public/ I would use full paths.

Most important reason why I am loading assets in this way, is if you are at some inner page of your site likecodingo.me/php-projects/super-laravel-app your browser will try to load styles from codingo.me/php-projects/assets/css/... cause there is no trailing slash in style include statement. Trailing slash in HTML includes will generate good links, no matter how deep your sites url structure is.

Now I will test this simple layout, but first I need to add route which will return this layout.


<?php

Route::get('/', function()
{
    return view('layouts.main');
});

Everything looks good, I have empty page with navigation links.

Layout Test

Layout Test

Login Page

Login view will be located in views/auth/login.blade.php and as I said I will use second Sign-in Page example from Bootstrap.


@extends('layouts.main')

@section('head')
    {!! HTML::style('/assets/css/signin.css') !!}
@stop

@section('content')


        {!! Form::open(['url' => '#', 'class' => 'form-signin' ] ) !!}


        @include('includes.status')

        <h2 class="form-signin-heading">Please sign in</h2>
        <label for="inputEmail" class="sr-only">Email address</label>
        {!! Form::email('email', null, ['class' => 'form-control', 'placeholder' => 'Email address', 'required', 'autofocus', 'id' => 'inputEmail' ]) !!}
        <label for="inputPassword" class="sr-only">Password</label>
        {!! Form::password('password', ['class' => 'form-control', 'placeholder' => 'Password', 'required',  'id' => 'inputPassword' ]) !!}

        <div class="checkbox">
            <label>
                {!! Form::checkbox('remember', 1) !!} Remember me

            </label>
        </div>
        <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
        <p><a href="#">Forgot password?</a></p>

        <p class="or-social">Or Use Social Login</p>

        <a href="#" class="btn btn-lg btn-primary btn-block facebook" type="submit">Facebook</a>
        <a href="#" class="btn btn-lg btn-primary btn-block twitter" type="submit">Twitter</a>

        {!! Form::close() !!}

@stop

From above code you could see that I am including status view from views/includes/status.blade.php This status view is in charge of displaying alerts to user, usually success messages.


@if(Session::has('message'))
    <div class="alert alert-{{ Session::get('status') }} status-box">
        <button type="button" class="close" data-dismiss="alert"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
        {{ Session::get('message') }}
    </div>
@endif

You can see that I am using Bootstrap alert, but you can use any kind of HTML. You can even display Sweet Alert dialog.

I am using same CSS from example page with some minor modifications. As you can see styling files are located in /assets/css/


.form-signin {
    max-width: 330px;
    padding: 15px;
    margin: 0 auto;
}
.form-signin .form-signin-heading,
.form-signin .checkbox {
    margin-bottom: 10px;
}
.form-signin .checkbox {
    font-weight: normal;
}
.form-signin .form-control {
    position: relative;
    height: auto;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
    padding: 10px;
    font-size: 16px;
}
.form-signin .form-control:focus {
    z-index: 2;
}
.form-signin input[type="email"] {
    margin-bottom: -1px;
    border-bottom-right-radius: 0;
    border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
    margin-bottom: 10px;
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}
.or-social{
    text-align:center;
    margin: 10px 0 10px 0;
}
.facebook{
    background-color: #4863ae;
    border-color: #4863ae;
}
.facebook:hover{
    background-color: #2871aa;
    border-color: #2871aa;
}
.twitter{
    background-color: #46c0fb;
    border-color: #46c0fb;
}
.twitter:hover{
    background-color: #00c7fb;
    border-color: #00c7fb;
}

I will test it in a same way as layout page, to cut time needed you can just replace return view('layouts.main');  with return view('auth.login');  in routes file. Here is the login page:

Login Page

Login Page

Password Reset Page

I will not attach images of password reset pages here cause they are trivial and very similar to Login page.


@extends('layouts.main')

@section('head')
    {!! HTML::style('/assets/css/reset.css') !!}
@stop

@section('content')

        {!! Form::open(['url' => '#', 'class' => 'form-signin' ] ) !!}

        @include('includes.status')

        <h2 class="form-signin-heading">Password Reset</h2>
        <label for="inputEmail" class="sr-only">Email address</label>
        {!! Form::email('email', null, ['class' => 'form-control', 'placeholder' => 'Email address', 'required', 'autofocus', 'id' => 'inputEmail' ]) !!}

        <br />
        <button class="btn btn-lg btn-primary btn-block" type="submit">Send me a reset link</button>

        {!! Form::close() !!}

@stop

All views related to login, register and passwords are in /views/auth/ folder.


.form-signin {
    max-width: 330px;
    padding: 15px;
    margin: 0 auto;
}
.form-signin .form-control {
    position: relative;
    height: auto;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
    padding: 10px;
    font-size: 16px;
}
.form-signin .form-control:focus {
    z-index: 2;
}

Password Reset Form Page

This page is displayed when user clicks on reset link. They will have 2 fields for new password and password confirmation.


@extends('layouts.main')

@section('head')
    {!! HTML::style('/assets/css/reset-form.css') !!}
@stop

@section('content')

        {!! Form::open(['url' => '#', ['token' => $token ]), 'class' => 'form-signin' ] ) !!}

        @include('includes.errors')

        <h2 class="form-signin-heading">Set New Password</h2>


        <label for="inputPassword" class="sr-only">Password</label>
        {!! Form::password('password', ['class' => 'form-control', 'placeholder' => 'Password', 'required',  'id' => 'inputPassword', 'autofocus' ]) !!}


        <label for="inputPasswordConfirmation" class="sr-only">Password Confirmation</label>
        {!! Form::password('password_confirmation', ['class' => 'form-control', 'placeholder' => 'Password confirmation', 'required',  'id' => 'inputPasswordConfirmation' ]) !!}


        <button class="btn btn-lg btn-primary btn-block" type="submit">Change</button>

        {!! Form::close() !!}

@stop


.form-signin {
    max-width: 330px;
    padding: 15px;
    margin: 0 auto;
}
.form-signin .form-control {
    position: relative;
    height: auto;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
    padding: 10px;
    font-size: 16px;
}
.form-signin .form-control:focus {
    z-index: 2;
}
.form-signin input {
    margin-bottom: -1px;
    border-radius:0px;
}
.form-signin #inputPassword {
    border-top-left-radius: 4px;
    border-top-right-radius: 4px;
}
.form-signin #inputPasswordConfirmation {
    margin-bottom: 10px;
    border-bottom-left-radius: 4px;
    border-bottom-right-radius: 4px;
}

In code above I include /views/includes/errors.blade.php with @include('includes.errors') . As you guess, this file is in charge of displaying error messages, usually from validators.


@if($errors->has())
    <div class="alert alert-danger fade in">
        <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
        <h4>Following errors occurred</h4>
        <ul>
            @foreach($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif

Register Page

For register form I will use same form as for login but I will add a few more fields and add custom css. It is good idea to create same social button on this page so users don't need to click more times than needed to sign-in.


@extends('layouts.main')

@section('head')
    {!! HTML::style('/assets/css/register.css') !!}
@stop

@section('content')

        {!! Form::open(['url' => '#', 'class' => 'form-signin' ] ) !!}

        @include('includes.errors')

        <h2 class="form-signin-heading">Please register</h2>

        <label for="inputEmail" class="sr-only">Email address</label>
        {!! Form::email('email', null, ['class' => 'form-control', 'placeholder' => 'Email address', 'required', 'autofocus', 'id' => 'inputEmail' ]) !!}

        <label for="inputFirstName" class="sr-only">First name</label>
        {!! Form::text('first_name', null, ['class' => 'form-control', 'placeholder' => 'First name', 'required', 'id' => 'inputFirstName' ]) !!}

        <label for="inputLastName" class="sr-only">Last name</label>
        {!! Form::text('last_name', null, ['class' => 'form-control', 'placeholder' => 'Last name', 'required', 'id' => 'inputLastName' ]) !!}


        <label for="inputPassword" class="sr-only">Password</label>
        {!! Form::password('password', ['class' => 'form-control', 'placeholder' => 'Password', 'required',  'id' => 'inputPassword' ]) !!}


        <label for="inputPasswordConfirm" class="sr-only">Confirm Password</label>
        {!! Form::password('password_confirmation', ['class' => 'form-control', 'placeholder' => 'Password confirmation', 'required',  'id' => 'inputPasswordConfirm' ]) !!}


        <button class="btn btn-lg btn-primary btn-block" type="submit">Register</button>

        <p class="or-social">Or Use Social Login</p>

        <a class="btn btn-lg btn-primary btn-block facebook" type="submit">Facebook</a>
        <a class="btn btn-lg btn-primary btn-block twitter" type="submit">Twitter</a>

        {!! Form::close() !!}


@stop


.form-signin {
    max-width: 330px;
    padding: 15px;
    margin: 0 auto;
}
.form-signin .form-signin-heading,
.form-signin .checkbox {
    margin-bottom: 10px;
}
.form-signin .checkbox {
    font-weight: normal;
}
.form-signin .form-control {
    position: relative;
    height: auto;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
    padding: 10px;
    font-size: 16px;
}
.form-signin .form-control:focus {
    z-index: 2;
}
.form-signin input[type="email"] {
    margin-bottom: -1px;
    border-bottom-right-radius: 0;
    border-bottom-left-radius: 0;
}
.form-signin input:not([type="email"]) {
    margin-bottom: -1px;
    border-radius:0px;
}
.form-signin #inputPasswordConfirm {
    margin-bottom: 10px;
    border-bottom-left-radius: 4px;
    border-bottom-right-radius: 4px;
}

.or-social{
    text-align:center;
    margin: 10px 0 10px 0;
}
.facebook{
    background-color: #4863ae;
    border-color: #4863ae;
}
.facebook:hover{
    background-color: #2871aa;
    border-color: #2871aa;
}
.twitter{
    background-color: #46c0fb;
    border-color: #46c0fb;
}
.twitter:hover{
    background-color: #00c7fb;
    border-color: #00c7fb;
}

You may notice that I am copying many same parts of css code and few pieces of UI over and over again. I am doing that intentionally, cause I am planning to refactor that code in next tutorial.

Register Page

Register Page

With this register page all authentication pages are now completed. Home page, admin panel and user panel pages are just simple placeholder pages that are extending main layout view.

Create migrations and models related to users and roles

Earlier I mentioned that this app will have 2 user types: ordinary user and administrator. I could create more roles but this is basic thing and you could easily extend it. Laravel comes with basic user table migration, it is quite good but for this initial tutorial I will need to make email column nullable, you will see later why. Also I will remove name column and instead of that add first_name and last_name columns. There is one more default migration there it is for password resets table, it is good I just need to add PK column $table->increments('id');

Now I will create tables related to roles, one will be named roles and other role_user commands are

php artisan make:migration create-roles --create=roles

php artisan make:migration create-role-user --create=role_user


<?php

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

class CreateRoles extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('roles', function (Blueprint $table) {
            $table->increments('id');
            $table->text('name');
            $table->timestamps();
        });
    }

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


<?php

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

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

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

As you can see role_user table is relationship table where I keep track of which role certain user has.

After this I am ready to run migrations php artisan migrate

Now I will create models for users and roles. Laravel does not posses dedicated folder for all models so I like to create it inapp\Models. Laravel comes with default User model, and I will need to move it inside Models folder and update namespace to App\Models. I need basic relationship method, some function to check does user posses certain role, method to assign certain role to new user and method to remove role. At the end of this model I will add roles related logic.

Cause I have modified User model namespace, I need to update authentication configuration file so Laravel can locate new model. Configuration is located in config/auth.php look for key 'model' and change its value to 'App\Models\User'.


    public function roles()
    {
        return $this->belongsToMany('App\Models\Role')->withTimestamps();
    }

    public function hasRole($name)
    {
        foreach($this->roles as $role)
        {
            if($role->name == $name) return true;
        }

        return false;
    }

    public function assignRole($role)
    {
        return $this->roles()->attach($role);
    }

    public function removeRole($role)
    {
        return $this->roles()->detach($role);
    }

Database seeders

First I will need to add basic 2 user roles to the database and after that I will create 2 users. I will assign different roles to them so it is important to first run Role seeder and only after that User seeder. All seeders are located inside database/seeds/ directory.


<?php

use Illuminate\Database\Seeder;
use App\Models\Role;

class RoleSeeder extends Seeder{

    public function run(){
        DB::table('roles')->delete();

        Role::create([
            'name'   => 'user'
        ]);

        Role::create([
            'name'   => 'administrator'
        ]);

    }
}

To be able to create roles in this way using mass-assignment I will need to add name column to fillable array of Role model like this protected $fillable = ['name'];


<?php

use Illuminate\Database\Seeder;
use App\Models\Role;
use App\Models\User;

class UserSeeder extends Seeder{

    public function run(){
        DB::table('users')->delete();

        $adminRole = Role::whereName('administrator')->first();
        $userRole = Role::whereName('user')->first();

        $user = User::create(array(
            'first_name'    => 'John',
            'last_name'     => 'Doe',
            'email'         => 'j.doe@codingo.me',
            'password'      => Hash::make('password')
        ));
        $user->assignRole($adminRole);

        $user = User::create(array(
            'first_name'    => 'Jane',
            'last_name'     => 'Doe',
            'email'         => 'jane.doe@codingo.me',
            'password'      => Hash::make('janesPassword')
        ));
        $user->assignRole($userRole);
    }
}

Now to include these seeder files into database seeder I need to call them from DatabaseSeeder.php file. You will find there commented example, just modify it and add RoleSeeder as first and UserSeeder as second call. After this I am ready to seed the database with php artisan db:seed


<?php

use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;

class DatabaseSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        Model::unguard();

         $this->call('RoleSeeder');
         $this->call('UserSeeder');

        Model::reguard();
    }
}

Middleware for administrator and user roles

When it comes to security I like to create simple solutions from default filters or use well-known packages like Sentry. For this I will modify existing Laravel's middleware located at app\Http\Middleware\Authenticate.php. 


    public function handle($request, Closure $next, $role)
    {
        if(!$this->auth->check())
        {
            return redirect()->route('auth.login')
                ->with('status', 'success')
                ->with('message', 'Please login.');
        }

        if($role == 'all')
        {
            return $next($request);
        }

        if( $this->auth->guest() || !$this->auth->user()->hasRole($role))
        {
            abort(403);
        }

        return $next($request);
    }

You can see that I am using named routes, because with them I can modify urls much faster. In this first if statement of handle method I am checking is user logged in at all, if not user is redirected to login page with appropriate message. I added next if statement cause I have certain routes that are same for both types of users, so I want to handle them in one place in routes file. And last if statement is checking does user posses certain role that is needed if not, application aborts.

Routes and Auth Controller

After I have created middleware it is good time to implement routes for our app and use that middleware. I will not use default AuthController instead I will create my own. Anyway, it will be located in same folder Auth so keep in mind that it's using App\Http\Controllers\Authnamespace. Cause this is dedicated namespace I will need to import all the facades.


    public function getLogin()
    {
        return view('auth.login');
    }

    public function postLogin()
    {
        $email      = Input::get('email');
        $password   = Input::get('password');
        $remember   = Input::get('remember');

        if($this->auth->attempt([
            'email'     => $email,
            'password'  => $password
        ], $remember == 1 ? true : false))
        {
            if( $this->auth->user()->hasRole('user'))
            {
                return redirect()->route('user.home');
            }

            if( $this->auth->user()->hasRole('administrator'))
            {
                return redirect()->route('admin.home');
            }

        }
        else
        {
            return redirect()->back()
                ->with('message','Incorrect email or password')
                ->with('status', 'danger')
                ->withInput();
        }

    }

    public function getLogout()
    {
        \Auth::logout();

        return redirect()->route('auth.login')
            ->with('status', 'success')
            ->with('message', 'Logged out');

    }

Post login method is accepting submits from login form and it checks user credentials. Later, based on user type, users are redirected to their home pages. In logout method I am terminating users session and logging him out. Routes file now looks like this:


<?php


$s = 'public.';
Route::get('/',         ['as' => $s . 'home',   'uses' => 'PagesController@getHome']);

$a = 'auth.';
Route::get('/login',            ['as' => $a . 'login',          'uses' => 'Auth\AuthController@getLogin']);
Route::post('/login',           ['as' => $a . 'login-post',     'uses' => 'Auth\AuthController@postLogin']);

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

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

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

As I mentioned before admin and user panel pages are basic placeholder pages and dedicated controllers AdminController and UserController are having only one method getHome() which returns respective view files.Same stands for home page, but I like to keep all public pages in separate controller. At the end I create common filter for both user types, actually I don't have all user type as you saw previously in Authenticate.php middleware, I have hard-coded that value.


<?php namespace App\Http\Controllers;

class AdminController extends Controller {

    public function getHome()
    {
        return view('panels.admin.home');
    }
}

One more thing is needed for this to work, I need to update login form action url and top navigation links to proper routes. After this login and logout should work.

Login is configured, now I can connect register form with backend.


    public function getRegister()
    {
        return view('auth.register');
    }

    public function postRegister()
    {
        $input = Input::all();
        $validator = Validator::make($input, User::$rules, User::$messages);
        if($validator->fails())
        {
            return redirect()->back()
                ->withErrors($validator)
                ->withInput();
        }

        $data = [
            'first_name'    => $input['first_name'],
            'last_name'     => $input['last_name'],
            'email'         => $input['email'],
            'password'      => $input['password']
        ];

        $this->userRepository->register($data);

        return redirect()->route('auth.login')
            ->with('status', 'success')
            ->with('message', 'You are registered successfully. Please login.');


    }

In postRegister method first thing that I check is input validation. I am using static arrays inside User model for this, they look like this:


    public static $rules = [
        'first_name'            => 'required',
        'last_name'             => 'required',
        'email'                 => 'required|email|unique:users',
        'password'              => 'required|min:6|max:20',
        'password_confirmation' => 'required|same:password'
    ];

    public static $messages = [
        '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'
    ];

It is pretty obvious now that I am using only server side validation, which is only 50% of job. In next part of this tutorial I will work with one of the best JS validation libraries and validate input on client side.

In postRegister method I am passing all form input into userRepositories method register. I have moved that logic into separated repository cause I am planning to hook many other services to it later and I like to keep my controllers clean. UserRepository is located inapp/Logic/User/UserRepository.php 


<?php namespace App\Logic\User;

use App\Models\Role;
use App\Models\User;
use Hash;

class UserRepository {

    public function register( $data )
    {

        $user = new User;
        $user->email            = $data['email'];
        $user->first_name       = ucfirst($data['first_name']);
        $user->last_name        = ucfirst($data['last_name']);
        $user->password         = Hash::make($data['password']);
        $user->save();

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

    }
}

All business related logic is separated in Logic directory, the best way is to use Interfaces and Repositories, but for now I am only using pure repositories.

From register method inside UserRepository you can see that I am creating new User object and assigning user role to that object.

I am not using any email validation or captcha for new user registrations, I have planned to add that in new tutorial. I will also show you how you can monetize each registration with one cool solution.

Only part left now is adding registration routes to routes file and updating registration forms and links in related views.


$a = 'auth.';
Route::get('/login',            ['as' => $a . 'login',          'uses' => 'Auth\AuthController@getLogin']);
Route::post('/login',           ['as' => $a . 'login-post',     'uses' => 'Auth\AuthController@postLogin']);
Route::get('/register',         ['as' => $a . 'register',       'uses' => 'Auth\AuthController@getRegister']);
Route::post('/register',        ['as' => $a . 'register-post',  'uses' => 'Auth\AuthController@postRegister']);

Password Reset Logic

Now when I have registration completed I will create password reset logic. For this I will use dedicated controller located inControllers/Auth/PasswordResetController.php.


<?php namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Logic\User\UserRepository;
use App\Models\User;
use App\Models\Password;
use Validator, Input, Hash;

class PasswordResetController extends Controller {

    public function getPasswordReset()
    {
        return view('auth.password-reset');
    }

    public function postPasswordReset( UserRepository $userRepository)
    {
        $rules = [
            'email' => 'email|required'
        ];

        $validator = Validator::make(Input::all(), $rules);
        if($validator->fails())
        {
            return redirect()->back()
                ->withErrors($validator)
                ->withInput();
        }

        $email  = Input::get('email');
        $user   = User::where('email', '=', $email)->first();
        if(empty($user))
        {
            return redirect()->back()
                ->withErrors(['User with this email does not exist']);
        }

        $userRepository->resetPassword( $user );

        return redirect()->back()
            ->with('status', 'success')
            ->with('message', 'Check your inbox!');

    }

    public function getPasswordResetForm( $token )
    {
        return view('auth.password-reset-form', compact('token'));
    }

    public function postPasswordResetForm( $token )
    {
        $rules = [
            'password'              => 'required|min:6|max:20',
            'password_confirmation' => 'required|same:password'
        ];

        $validator = Validator::make(Input::all(), $rules);
        if($validator->fails())
        {
            return redirect()->back()
                ->withErrors($validator);
        }

        $password = Password::where('token', '=', $token)->first();
        if(empty($password))
        {
            return view('pages.status')
                ->with('error', 'Reset token is invalid');
        }

        $user = User::where('email', '=', $password->email)->first();
        $user->password = Hash::make(Input::get('password'));
        $user->save();

        $password->delete();

        return redirect()->route('auth.login')
            ->with('status', 'success')
            ->with('message', 'Password changed successfully!');
    }

}

After user posts new password reset request, backend method checks for validation errors first, then it checks is user with associated email present in the database and if everything is fine it passes user object to userRepository. What's resetPassword method is doing is pretty straightforward, it is creating new row in password_resets table and sending reset link to user mailbox.

For sending emails I have created separated logic in Logic/Mailers with 2 classes for now:

  • Mailer.php - general email sending logic
  • UserMailer.php - user related email sending logic

I am using this approach cause I plan to add admin mailer, so I want to reuse common email logic. Also I will add email verification logic for users so there is no need for admin to have that logic. So the best option is to create abstract Mailer class with sendTo method and extend that class in User and Admin mailers.


<?php namespace App\Logic\Mailers;

abstract class Mailer {

    public function sendTo($email, $subject, $fromEmail, $view, $data = [])
    {
        \Mail::queue($view, $data, function($message) use($email, $subject, $fromEmail)
        {

            $message->from($fromEmail, 'tuts@codingo.me');

            $message->to($email)
                ->subject($subject);
        });
    }
}


<?php namespace App\Logic\Mailers;

class UserMailer extends Mailer {

    public function passwordReset($email, $data)
    {
        $view       = 'emails.password-reset';
        $subject    = $data['subject'];
        $fromEmail  = 'tuts@codingo.me';

        $this->sendTo($email, $subject, $fromEmail, $view, $data);
    }

}

For this password reset email I am using basic text without any fancy templates.


<p style="text-align: left;font-size:16px;">Hi {{ $first_name }},</p>

<p style="text-align: left;font-size:16px;">Recently you requested password reset link for your Codingo Tuts account. If you did not request password reset, then please ignore this email.</p>

<p style="text-align: left;font-size:16px;">Please click on following link <a target="_blank" href="{{ route('auth.reset', ['token' => $token]) }}">Reset Password</a>.</p>


<p style="text-align: left;font-size:15px;">Sincerely,</p>

<p style="text-align: left;font-size:15px;">Codingo Support</p>

Finally I will add resetPassword method to userRepository so system can generate new row in password_resets table and send email.  I am using Password model for storing data in password_resets table.


<?php namespace App\Logic\User;

use App\Logic\Mailers\UserMailer;
use App\Models\Role;
use App\Models\User;
use App\Models\Password;
use Hash, Carbon\Carbon;

class UserRepository {

    protected $userMailer;

    public function __construct( UserMailer $userMailer )
    {
        $this->userMailer = $userMailer;
    }

    public function register( $data )
    {

        $user = new User;
        $user->email            = $data['email'];
        $user->first_name       = ucfirst($data['first_name']);
        $user->last_name        = ucfirst($data['last_name']);
        $user->password         = Hash::make($data['password']);
        $user->save();

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

    }

    public function resetPassword( User $user  )
    {
        $token = sha1(mt_rand());
        $password = new Password;
        $password->email = $user->email;
        $password->token = $token;
        $password->created_at = Carbon::now();
        $password->save();

        $data = [
            'first_name'    => $user->first_name,
            'token'         => $token,
            'subject'       => 'Example.com: Password Reset Link',
            'email'         => $user->email
        ];

        $this->userMailer->passwordReset($user->email, $data);
    }
}

For sending emails I am using Mandrill, everything that needs to be done is generating new set of API keys and inserting those values in.env file. Set of keys from .env.example file won't work cause I disabled them.

Contacting 3rd party service like I contact Mandrill here, could cause delays. These delays are very annoying to end users, even if they take only 2-3 seconds. Checkout my other tutorial where I explain how you can code email queue Sending emails over Queue with AWS SES using this same codebase.

As usual now I need to add password reset routes to routes file and update view with valid links.


$a = 'auth.';
Route::get('/login',            ['as' => $a . 'login',          'uses' => 'Auth\AuthController@getLogin']);
Route::post('/login',           ['as' => $a . 'login-post',     'uses' => 'Auth\AuthController@postLogin']);
Route::get('/register',         ['as' => $a . 'register',       'uses' => 'Auth\AuthController@getRegister']);
Route::post('/register',        ['as' => $a . 'register-post',  'uses' => 'Auth\AuthController@postRegister']);
Route::get('/password',         ['as' => $a . 'password',       'uses' => 'Auth\PasswordResetController@getPasswordReset']);
Route::post('/password',        ['as' => $a . 'password-post',  'uses' => 'Auth\PasswordResetController@postPasswordReset']);
Route::get('/password/{token}', ['as' => $a . 'reset',          'uses' => 'Auth\PasswordResetController@getPasswordResetForm']);
Route::post('/password/{token}',['as' => $a . 'reset-post',     'uses' => 'Auth\PasswordResetController@postPasswordResetForm']);

Create table and model for Social logins

I am planning to use users table for name and email data and put all social login related data into social_logins table. Socialite will return user unique social id and I will use that and social provider to determine is user new or existing one.

php artisan make:migration create-social-logins --create=social_logins


<?php

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

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

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

After this I can run migration php artisan migrate

Provider will be string like: facebook, twitter, github etc. And socialite uses configurations stored in config/services.php file. Everything that is needed is creating new array element for each new social provider. This is a part of my services.php file:


    'facebook' => [
        'client_id'     => env('FB_ID'),
        'client_secret' => env('FB_SECRET'),
        'redirect'      => env('FB_REDIRECT')
    ],

    'twitter' => [
        'client_id'     => env('TW_ID'),
        'client_secret' => env('TW_SECRET'),
        'redirect'      => env('TW_REDIRECT')
    ],

I am storing social app related data in .env file.

Checkout Create your first Facebook application to see how I created new FB application and acquired these client_id and client_secret tokens.

Model for social logins is pretty simple, it has only one relationship for user.

<?php namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Social extends Model {

    protected $table = 'social_logins';

    public function user()
    {
        return $this->belongsTo('App\Models\User');
    }
}

Create Social Logic

When we want to authenticate user with one of Socialite providers, first we redirect that user to social site and after that social site redirects user back to our server with certain tokens. In the back Socialite contacts social site once more with those tokens and accepts user object if everything is OK. Here I will handle only social redirects when user allows our app to read their social data, in next tutorial I will cover cancelations.

I want this social logic to be very extensible so I can add new providers in matter of seconds. Because of that I will not hardcode any values in routes. These are my social routes:


$s = 'social.';
Route::get('/social/redirect/{provider}',   ['as' => $s . 'redirect',   'uses' => 'Auth\AuthController@getSocialRedirect']);
Route::get('/social/handle/{provider}',     ['as' => $s . 'handle',     'uses' => 'Auth\AuthController@getSocialHandle']);

So social buttons will use following links /social/redirect/facebook and /social/redirect/twitter. When user visits them, system will trigger Socialite redirection and it will get user object in return. You will notice redirect key for each social provider in services.php, that url social site will use as callback url. And in my system that route is /social/handle/facebook or /social/handle/twitter

Now you can see that adding new provider is matter of inserting new element in services.php and creating dedicated social button in views.


    public function getSocialRedirect( $provider )
    {
        $providerKey = \Config::get('services.' . $provider);
        if(empty($providerKey))
            return view('pages.status')
                ->with('error','No such provider');

        return Socialite::driver( $provider )->redirect();

    }

    public function getSocialHandle( $provider )
    {
        $user = Socialite::driver( $provider )->user();

        $socialUser = null;

        //Check is this email present
        $userCheck = User::where('email', '=', $user->email)->first();
        if(!empty($userCheck))
        {
            $socialUser = $userCheck;
        }
        else
        {
            $sameSocialId = Social::where('social_id', '=', $user->id)->where('provider', '=', $provider )->first();

            if(empty($sameSocialId))
            {
                //There is no combination of this social id and provider, so create new one
                $newSocialUser = new User;
                $newSocialUser->email              = $user->email;
                $name = explode(' ', $user->name);
                $newSocialUser->first_name         = $name[0];
                $newSocialUser->last_name          = $name[1];
                $newSocialUser->save();

                $socialData = new Social;
                $socialData->social_id = $user->id;
                $socialData->provider= $provider;
                $newSocialUser->social()->save($socialData);

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

                $socialUser = $newSocialUser;
            }
            else
            {
                //Load this existing social user
                $socialUser = $sameSocialId->user;
            }

        }

        $this->auth->login($socialUser, true);

        if( $this->auth->user()->hasRole('user'))
        {
            return redirect()->route('user.home');
        }

        if( $this->auth->user()->hasRole('administrator'))
        {
            return redirect()->route('admin.home');
        }

        return \App::abort(500);
    }

In getSocialRedirect method I am passing provider string as parameter and I am checking is that provider present in services. If it is present then system redirects user to social site.

In getSocialHandle I am catching data which social site sends to the server.

  • First I am checking is user with same email present in users table, if yes then I login that user and system redirects him to proper panel.
  • Else I check is user with same social id and provider present in social_logins table
    • If user is present I just login that user
    • Else I create that new user and associate respective social data to that account. I am also attaching user roles here.

This approach posses one downside, for example Twitter does not pass users email to us. So that's why I changed email field to be nullable. I will add one simple trick in next tutorial for fixing this.

source codingo

Related Posts