Hello all,
I've got a project where I'm using Laravel Fortify with 2 factor authentication. I've got the enable and disable functionality along with the confirmation working. I'm also using Laravel Socialite to make it easier for users to login/logout. I've got this working separately as well.
The problem is that I have some legal requirements that require 2 factor authentication. So I've been trying to get 2 factor authentication to work with Socialite.
What are some best practices around implementing 2 factor authentication with oauth? I know that some oauth providers give their own version of 2 factor authentication but I believe that depends on the user's settings right?
For now I've implemented a middleware
class CustomTwoFactorMiddleware
{
/**
* The guard implementation.
*
* @var \Illuminate\Contracts\Auth\StatefulGuard
*/
protected $guard;
/**
* The login rate limiter instance.
*
* @var \Laravel\Fortify\LoginRateLimiter
*/
protected $limiter;
/**
* Create a new controller instance.
*
* @param \Illuminate\Contracts\Auth\StatefulGuard $guard
* @param \Laravel\Fortify\LoginRateLimiter $limiter
* @return void
*/
public function __construct(StatefulGuard $guard, LoginRateLimiter $limiter)
{
Log::info("Creating middleware");
$this->guard = $guard;
$this->limiter = $limiter;
}
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @param callable $next
* @return mixed
*/
public function handle($request, $next)
{
Log::info("handling validation");
// $user = $this->validateCredentials($request);
//Assume that user has already authenticated and we are checking for two factor validation only.
$user = $request->user();
if (Fortify::confirmsTwoFactorAuthentication()) {
if (optional($user)->two_factor_secret &&
! is_null(optional($user)->two_factor_confirmed_at) &&
in_array(TwoFactorAuthenticatable::class, class_uses_recursive($user)) &&
$request->hasChallengedUser() == false
) {
Log::info("Checking for two factor");
return $this->twoFactorChallengeResponse($request, $user);
} else {
Log::info("two factor skipped");
return $next($request);
}
}
if (optional($user)->two_factor_secret &&
in_array(TwoFactorAuthenticatable::class, class_uses_recursive($user)) &&
$request->hasChallengedUser() == false
) {
Log::info("two factor skipped");
return $this->twoFactorChallengeResponse($request, $user);
}
Log::info("two factor skipped");
return $next($request);
}
/**
* Attempt to validate the incoming credentials.
*
* @param \Illuminate\Http\Request $request
* @return mixed
*/
protected function validateCredentials($request)
{
if (Fortify::$authenticateUsingCallback) {
Log::info(print_r(Fortify::$authenticateUsingCallback,true));
return tap(call_user_func(Fortify::$authenticateUsingCallback, $request), function ($user) use ($request) {
if (! $user) {
$this->fireFailedEvent($request);
$this->throwFailedAuthenticationException($request);
}
});
}
$model = $this->guard->getProvider()->getModel();
return tap($model::where(Fortify::username(), $request->{Fortify::username()})->first(), function ($user) use ($request) {
if (! $user || ! $this->guard->getProvider()->validateCredentials($user, ['password' => $request->password])) {
$this->fireFailedEvent($request, $user);
$this->throwFailedAuthenticationException($request);
}
});
}
/**
* Throw a failed authentication validation exception.
*
* @param \Illuminate\Http\Request $request
* @return void
*
* @throws \Illuminate\Validation\ValidationException
*/
protected function throwFailedAuthenticationException($request)
{
$this->limiter->increment($request);
throw ValidationException::withMessages([
Fortify::username() => [trans('auth.failed')],
]);
}
/**
* Fire the failed authentication attempt event with the given arguments.
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
* @return void
*/
protected function fireFailedEvent($request, $user = null)
{
event(new Failed(config('fortify.guard'), $user, [
Fortify::username() => $request->{Fortify::username()},
'password' => $request->password,
]));
}
/**
* Get the two factor authentication enabled response.
*
* @param \Illuminate\Http\Request $request
* @param mixed $user
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function twoFactorChallengeResponse($request, $user)
{
Log::info("Attempting to route to two-factor auth");
$request->session()->put([
'login.id' => $user->getKey(),
'login.remember' => $request->boolean('remember'),
'doing_two_factor' => true
]);
TwoFactorAuthenticationChallenged::dispatch($user);
return $request->wantsJson()
? response()->json(['two_factor' => true])
: redirect()->route('two-factor.login');
}
}
This works and I am able to enable two factor authentication.
But I have run into an issue on the two-factor challenge where the application doesn't have a ChallengeUser because authentication isn't from Fortify but Socialite resulting in application perpetually redirecting to the two-factor challenge.. Is this the right approach or is there a better way to implement this?
Thank you for any assistance you can provide.