Laravel Google2FA


The intent of this tutorial is to create QRCodes for Google2FA and check User typed codes. Having Two-Factor Authentication provides better security to ones Account allowing Users only having access through Account credentials and Google Authenticator App codes.

  1. Packages
  2. Migration, Model and Controller
  3. Middleware
  4. Routes
  5. Views
  6. Error handling
  7. References

Installing Packages

The pragmarx/google2fa-laravel package supports the Time-based One-time Password (TOTP) algorithm, allowing for Users to login once during a single session. Installation is simple and the package is supported by another called bacon/bacon-qr-code to generate QRCodes so that Users may scan it into their Google Authenticator App to auto-generate codes to login.

Now publish the config file and change the One-Time Password view to auth.2fagoogle. This view will be auto-loaded if the User has set up their Two-Factor Authentication and after the User logs in with their credentials.

Model, Migration and Controller

To handle Authentication of multiple Users we need to make a Model, Migration and Controller to handle the methods that are required.

Model:

For the Model, target the new table, make all columns fillable and create the relationship between the Google2FA and User.

class PasswordSecurity extends Model
{
  //Table Name
  protected $table = 'password_securities';

  /**
   * The attributes that are mass assignable.
   *
   * @var array
   */
  protected $fillable = [
      'user_id', 'google2fa_enable', 'google2fa_secret',
  ];

  // A Google2FA belongsTo a user
  public function user()
  {
    return $this->belongsTo('App\User');
  }
}
  // A User hasOne Google2FA
  public function PasswordSecurity()
  {
    return $this->hasOne('App\PasswordSecurity');
  }

Migration:

For the migration we need to associate the User with their Google2FA, so we need the foreign key, we need to determine whether their Google2FA is enabled or not and we need the QRCode secret.

class CreatePasswordSecuritiesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('password_securities', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('user_id');
            $table->boolean('google2fa_enable')->default(false);
            $table->string('google2fa_secret')->nullable();
            $table->timestamps();
        });
    }

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

Controller:

For the Controller we only need four methods, one to be able to generate the QRCode for the User (show2faForm), another to Generate a Secret Code if the User does not already have one from the QRCode (generate2faSecretCode), to Enable the User's Two-Factor Authentication (enable2fa) and to disable it (disable2fa). Also remember to include the package in this file use PragmaRX\Google2FA\Google2FA;.

class PasswordSecurityController extends Controller
{

  public function show2faForm()
  {
    if (Auth::guest()){
      return;
    }

    $user = Auth::user();

    $google2FaUrl = '';

    // If User has 2FA current disabled generate QR code
    if (count($user->PasswordSecurity)) {

      $google2Fa = new Google2FA();
      $google2Fa->setAllowInsecureCallToGoogleApis(true);
      $google2FaUrl = $google2Fa->getQRCodeGoogleUrl(
        $user->name,
        $user->email,
        $user->PasswordSecurity->google2fa_secret
      );

    }

    $data = array(
      'user' => $user,
      'google2FaUrl' => $google2FaUrl
    );

    return view('auth.google2fa')->with('data', $data);
  }


  public function generate2faSecretCode(Request $request)
  {
    $user = Auth::user();

    $google2Fa = new Google2FA();

    // Generate a new Google2FA code for User
    PasswordSecurity::create([
      'user_id' => $user->id,
      'google2fa_enable' => 0,
      'google2fa_secret' => $google2Fa->generateSecretKey()
    ]);

    return redirect('/2fa')->with('success', 'Success, you secret key has been generated. Please verify to enable');
  }


  public function enable2fa(Request $request)
  {
    $user = Auth::user();

    // Enable Google2FA if Google Authenticator code matches secret
    $google2Fa = new Google2FA();
    $secret = $request->input('verifyCode');
    $valid = $google2Fa->verifyKey($user->PasswordSecurity->google2fa_secret, $secret);

    // If Google2FA code is valid enable Google2FA
    if($valid) {
      $user->PasswordSecurity->google2fa_enable = 1;
      $user->PasswordSecurity->save();
      return redirect('/2fa')->with('success', 'Success, 2FA is anabled.');

    // Else redirect with invalid code error
    } else {
      return redirect('/2fa')->with('error', 'Invalid Code, try again.');
    }

  }


  public function disable2fa(Request $request)
  {
    $user = Auth::user();

    // If user disables Google2FA check User password credentials
    if ( !( Hash::check($request->currentPassword, $user->password)) ) {
      return redirect('/2fa')->with('error', 'Your password does not match!');
    }

    // Update disabling Google2FA
    $user->PasswordSecurity->google2fa_enable = 0;
    $user->PasswordSecurity->save();

    return redirect('/2fa')->with('success', 'Success 2FA is disabled.');
  }

}

Middleware

The Middleware will allow the User to login with or without their Two-Factor Authentication. The way we achieve this is by autoloading the file in the routes. This file will check the functions within the Google2FAAuthentication class.

class Google2FAMiddleware
{
  /**
   * Handle an incoming request.
   *
   * @param  \Illuminate\Http\Request  $request
   * @param  \Closure  $next
   * @param  string|null  $guard
   * @return mixed
   */
  public function handle($request, Closure $next)
  {
      // Send requested loggin User to Google2FA Authentication Support
      $authentication = app(Google2FAAuthentication::class)->boot($request);

      // (true)
      if ($authentication->isAuthenticated()) {

          // If User is Authenticated through 2FA then proceed
          return $next($request);
      }

      // (false)
      // If User is not Authenticated through 2FA return action getGoogle2FaSecretkey() on Google2FAAuthentication::class
      return $authentication->makeRequestOneTimePasswordResponse();
  }
}  
class Google2FAAuthentication extends Authenticator
{
  // If User does not have Google2FA Setup yet
  protected function canPassWithoutCheckingOTP()
  {
    if(!count($this->getUser()->PasswordSecurity))
    {
      return true;
    }
    return !$this->getUser()->PasswordSecurity->google2fa_enable || !$this->isEnabled() || $this->noUserIsAuthenticated() || $this->twoFactorAuthStillValid();
  }

  protected function getGoogle2FaSecretkey()
  {
    // Get User secret column
    try {
      $secret = $this->getUser()->PasswordSecurity->{$this->config('otp_secret_column')};
    } catch (\Exception $e) {
      // If User has not set up Google2FA
      $secret = $this->getUser()->PasswordSecurity;
    }

    // If User is not Authenticated through 2FA
    if(is_null($secret) || empty($secret)) {

      // return Action
      return redirect()->action('PasswordSecurityController@generate2faSecretCode');
    }

    // If user has Google2FA setup and is Authenticated
    return $secret;
  }
}

Remember to include use PragmaRX\Google2FALaravel\Support\Authenticator; in the Google2FAAuthentication file!

Routes

There are 6 total Routes that are required, four to handle all of the Controller methods previous mentioned and two to handle the validation of the User's Two-Factor Authentication. Remember if this package is to be included in the rest of the Routes, create a Route Group and call the Middleware into the array (Route::group(['middleware' => ['auth', '2fa']]).

/*
--------------------------------------------------------------------------
Google2FA
--------------------------------------------------------------------------
*/

// Display 2FA form if User has generated Google2FA
Route::get('/2fa', 'PasswordSecurityController@show2faForm');

// Generate a new Google2FA code if a User does not already have one
Route::post('/generate2faSecret', [
  'uses' => 'PasswordSecurityController@generate2faSecretCode',
  'as'   => 'generate2faSecretCode'
]);

// Enable 2FA for User
Route::post('/enable2fa', [
  'uses' => 'PasswordSecurityController@enable2fa',
  'as'   => 'enable2fa'
]);

// Disable 2FA for User
Route::post('/disable2fa', [
  'uses' => 'PasswordSecurityController@disable2fa',
  'as'   => 'disable2fa'
]);

// Verify 2FA if User has it enabled
Route::post('/verify2fa', function() {
  return view('home');
})->name('verify2fa')->middleware('2fa');


Route::get('/verify2fa', function() {
  return redirect(URL()->previous());
})->name('verify2fa')->middleware('2fa');

/*
--------------------------------------------------------------------------
Google2FA
--------------------------------------------------------------------------
*/

Views

Now we need to generate the necessary views to be able to handle all the Controller methods and for the User to login with their Google2FA code from the Google Authenticator App.

<!-- 2fagoogle -->
<form method="POST" action="{{ route('verify2fa') }}">
  @csrf

  <div class="form-row">
    <div class="form-group col-sm-4">
      <p><b>One Time Password:</b></p>
    </div>
    <div class="form-group col-sm-8">
      <input type="password" class="form-control" name="one_time_password" id="one_time_password" required>
    </div>
  </div>

  <div class="form-group">
    <button type="submit" class="btn btn-primary pull-left" name="button">Proceed</button>
  </div>
</form>
<!-- google2fa -->
@if(!count($data['user']->PasswordSecurity))

<p>Enable 2FA on your account.</p>
<p>Click the generate secret button to generate your code</p>
<p>Enter this into Googel Authenticator App</p>

<form method="POST" action="{{ route('generate2faSecretCode') }}">
  @csrf
  <div class="form-group">
    <button type="submit" class="btn btn-info" name="button">Generate Secret Key</button>
  </div>
</form>

@elseif(!$data['user']->PasswordSecurity->google2fa_enable)

<p>Two factor authentication (2FA) strengthens access security by requiring two methods (also referred to as factors)
  to verify your identity.</p>

  <p>To Enable Two Factor Authentication on your Account, you need to do the following steps:</p>

  <p><b>1. Scan this barcode with your Google Authenticator App:</b></p>
  <img src="{{ $data['google2FaUrl'] }}">
  <p><b>2. Enter the pin to Enable 2FA:</b></p>

  <form method="POST" action="{{ route('enable2fa') }}">
    @csrf

    <div class="form-row">
      <div class="form-group col-sm-4">
        <p><b>Authenticator Code:</b></p>
      </div>
      <div class="form-group col-sm-8">
        <input type="password" class="form-control" name="verifyCode" id="verifyCode" required>
      </div>
    </div>

    <div class="form-group">
      <button type="submit" class="btn btn-success" name="button">Enable 2FA</button>
    </div>
  </form>

  @elseif($data['user']->PasswordSecurity->google2fa_enable)

  <p><b>2FA is enabled.</b></p>
  <p>If you are looking to disable Two Factor Authentication. Please confirm your password and Click Disable 2FA below.</p>

  <form method="POST" action="{{ route('disable2fa') }}">
    @csrf
    <div class="form-group">
      <input type="password" class="form-control" name="currentPassword" id="currentPassword" required>
    </div>
    <div class="form-group">
      <button type="submit" class="btn btn-primary" name="button">Disable 2FA</button>
    </div>
  </form>

  @endif

Error handling

If you encounter an error or two consider the following terminal commands that may help you to compile your build proper or configure the build's dependancies properly if not already

References