I am trying to create an app where the user has the option to connect with google2fa. It enables the option on it’s profile and then google2fa becomes active.
However, after implementing this tutorial https://www.5balloons.info/two-factor-authentication-google2fa-laravel-5/ and this one https://github.com/antonioribeiro/google2fa-laravel , I end up with the following routes
Route::get('/dashboard', 'PagesController@dashboard')->middleware(['auth', '2fa']);
Route::get('/complete-registration', 'AuthRegisterController@completeRegistration');
Route::get('/', 'HomeController@index')->middleware(['auth', '2fa']);
Route::get('/2fa/enable', 'Google2FAController@enableTwoFactor');
Route::get('/2fa/disable', 'Google2FAController@disableTwoFactor');
Route::get('/2fa/validate', 'AuthAuthController@getValidateToken');
Route::post('/2fa/validate', ['middleware' => 'throttle:5', 'uses' => 'AuthAuthController@postValidateToken']);
Google2FAMiddleware.php
<?php
namespace AppHttpMiddleware;
use AppSupportGoogle2FAAuthenticator;
use IlluminateContractsAuthGuard;
use Closure;
class Google2FAMiddleware
{
/**
* Handle an incoming request.
*
* @param IlluminateHttpRequest $request
* @param Closure $next
* @return mixed
*/
protected $auth;
public function __construct(Guard $auth)
{
$this->auth = $auth;
}
public function handle($request, Closure $next)
{
$authenticator = app(Google2FAAuthenticator::class)->boot($request);
if ($authenticator->isAuthenticated()) {
return $next($request);
}
return $authenticator->makeRequestOneTimePasswordResponse();
}
}
Google2FAAuthenticator.php
<?php
namespace AppSupport;
use PragmaRXGoogle2FALaravelSupportAuthenticator;
class Google2FAAuthenticator extends Authenticator
{
protected function canPassWithoutCheckingOTP()
{
if(!count($this->getUser()->google2fa_secret))
return true;
return
!$this->getUser()->use2fa ||
!$this->isEnabled() ||
$this->noUserIsAuthenticated() ||
$this->twoFactorAuthStillValid();
}
protected function getGoogle2FASecretKey()
{
$secret = $this->getUser()->{$this->config('otp_secret_column')};
if (is_null($secret) || empty($secret)) {
throw new InvalidSecretKey('Secret key cannot be empty.');
}
return $secret;
}
}
User.php model
//google2fa encryption
public function setGoogle2faSecretAttribute($value){
$this->attributes['google2fa_secret'] = encrypt($value);
}
// google2fa decryption
public function getGoogle2faSecretAttribute($value){
// dd(decrypt($value));
return decrypt($value);
}
public function passwordSecurity()
{
return $this->hasOne('AppPasswordSecurity');
}
Google2FAController.php
<?php
namespace AppHttpControllers;
use Crypt;
use Google2FA;
use IlluminateHttpRequest;
use AppHttpControllersController;
use IlluminateFoundationValidationValidatesRequests;
use ParagonIEConstantTimeBase32;
use IlluminateSupportFacadesAuth;
class Google2FAController extends Controller
{
use ValidatesRequests;
public function __construct()
{
$this->middleware('web');
}
public function enableTwoFactor(Request $request)
{
//generate new secret
$secret = $this->generateSecret();
//get user
$user = Auth::user();
//encrypt and then save secret
$user->google2fa_secret = Crypt::encrypt($secret);
$user->save();
//generate image for QR barcode
$imageDataUri = Google2FA::getQRCodeInline(
$request->getHttpHost(),
$user->email,
$secret,
200
);
// return view('user.profile', ['image' => $imageDataUri,
// 'secret' => $secret, 'use2fa' => 1]);
return response()->json([
'use2fa' => 1
]);
}
public function disableTwoFactor(Request $request)
{
$user = $request->user();
//make secret column blank
$user->google2fa_secret = null;
$user->save();
// return view('2fa/disableTwoFactor');
return response()->json([
'use2fa' => 0
]);
}
/**
* Generate a secret key in Base32 format
*
* @return string
*/
private function generateSecret()
{
$randomBytes = random_bytes(10);
return Base32::encodeUpper($randomBytes);
}
}
config file for google2fa
<?php
return [
/*
* Enable / disable Google2FA.
*/
'enabled' => true,
/*
* Lifetime in minutes.
*
* In case you need your users to be asked for a new one time passwords from time to time.
*/
'lifetime' => 0, // 0 = eternal
/*
* Renew lifetime at every new request.
*/
'keep_alive' => true,
/*
* Auth container binding.
*/
'auth' => 'auth',
/*
* 2FA verified session var.
*/
'session_var' => 'google2fa',
/*
* One Time Password request input name.
*/
'otp_input' => 'one_time_password',
/*
* One Time Password Window.
*/
'window' => 1,
/*
* Forbid user to reuse One Time Passwords.
*/
'forbid_old_passwords' => false,
/*
* User's table column for google2fa secret.
*/
'otp_secret_column' => 'google2fa_secret',
/*
* One Time Password View.
*/
'view' => 'google2fa.index',
/*
* One Time Password error message.
*/
'error_messages' => [
'wrong_otp' => "The 'One Time Password' typed was wrong.",
'cannot_be_empty' => 'One Time Password cannot be empty.',
'unknown' => 'An unknown error has occurred. Please try again.',
],
/*
* Throw exceptions or just fire events?
*/
'throw_exceptions' => true,
/*
* Which image backend to use for generating QR codes?
*
* Supports imagemagick, svg and eps
*/
'qrcode_image_backend' => PragmaRXGoogle2FALaravelSupportConstants::QRCODE_IMAGE_BACKEND_IMAGEMAGICK,
];
After enabling google 2 factor, after login, I get this error:
IlluminateContractsEncryptionDecryptException
The payload is invalid.
If I comment the getGoogle2faSecretAttribute function from the User.php model, I get this error:
Route [2fa] not defined
I don’t know what I’m missing here.
P.S: Laravel6, latest version of php, latest version of mysql
2
Answers
It’s because you haven’t encrypted your user secret value when storing it, so when you are decrypting it, probably inside
getGoogle2faSecretAttribute($value)
method in your App/User class, it’s already decrypted therefore you are receiving
The payload is invalid
from decryptor as it can’t decrypt already decrypted value.I hit the same problem. The issue is the accessor for the
google2fa_secret
field on the User model:If, like me, you’re just setting this up on an application with existing users, obviously none of them have 2FA set up yet, and everyone’s
google2fa_secret
field in the DB will benull
. So if you try to access the User’sgoogle2fa_secret
, say like you do here:The accessor tries to
decrypt(null)
– which will throw the error you are seeing!You can confirm it in Tinker:
What I did is modify the accessor to allow nulls, like so: