Fortify will not perform 2FA when documentation is followed.
ChrisThompsonTLDR opened this issue · comments
- Fortify Version: 1.8.0
- Laravel Version: 8.55.0
- PHP Version: 7.4.11
- Database Driver & Version: MySQL 8
Description:
Following the Laravel, Fortify and Breeze installation docs, with the 2FA steps, the application will never ask a user for their 2FA code when logging in, even after the user has enabled 2FA.
Steps To Reproduce:
Install Laravel
composer create-project laravel/laravel ./
- update
.env
to have database access
Install Fortify
composer require laravel/fortify
php artisan vendor:publish --provider="Laravel\Fortify\FortifyServiceProvider"
php artisan migrate
- Follow the Two Factor Authentication steps.
Install Breeze
I know this isn't required, but it helps test the bug.
composer require laravel/breeze --dev
php artisan breeze:install
npm install
npm run dev
php artisan migrate
Test
- visit the site
- register a user
- activate 2fa
- logout
- login (you will not be asked for your 2fa code
Why does this happen
The AuthenticatedSessionController installed by laravel/breeze
does not contain the logic required by laravel/fortify
to ask the user for their 2FA code.
Compare the store()
method in this controller in laravel/breeze
:
public function store(LoginRequest $request)
{
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(RouteServiceProvider::HOME);
}
with that of AuthenticatedSessionController found in laravel/fortify
public function store(LoginRequest $request)
{
return $this->loginPipeline($request)->then(function ($request) {
return app(LoginResponse::class);
});
}
protected function loginPipeline(LoginRequest $request)
{
if (Fortify::$authenticateThroughCallback) {
return (new Pipeline(app()))->send($request)->through(array_filter(
call_user_func(Fortify::$authenticateThroughCallback, $request)
));
}
if (is_array(config('fortify.pipelines.login'))) {
return (new Pipeline(app()))->send($request)->through(array_filter(
config('fortify.pipelines.login')
));
}
return (new Pipeline(app()))->send($request)->through(array_filter([
config('fortify.limiters.login') ? null : EnsureLoginIsNotThrottled::class,
Features::enabled(Features::twoFactorAuthentication()) ? RedirectIfTwoFactorAuthenticatable::class : null,
AttemptToAuthenticate::class,
PrepareAuthenticatedSession::class,
]));
}
When my application's app/Http/Controllers/Auth/AuthenticatedSessionController.php
is:
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Pipeline\Pipeline;
use Illuminate\Support\Facades\Auth;
use Laravel\Fortify\Actions\AttemptToAuthenticate;
use Laravel\Fortify\Actions\EnsureLoginIsNotThrottled;
use Laravel\Fortify\Actions\PrepareAuthenticatedSession;
use Laravel\Fortify\Actions\RedirectIfTwoFactorAuthenticatable;
use Laravel\Fortify\Contracts\LoginResponse;
use Laravel\Fortify\Features;
use Laravel\Fortify\Fortify;
use Laravel\Fortify\Http\Requests\LoginRequest;
class AuthenticatedSessionController extends Controller
{
/**
* Display the login view.
*
* @return \Illuminate\View\View
*/
public function create()
{
return view('auth.login');
}
/**
* Attempt to authenticate a new session.
*
* @param \Laravel\Fortify\Http\Requests\LoginRequest $request
* @return mixed
*/
public function store(LoginRequest $request)
{
return $this->loginPipeline($request)->then(function ($request) {
return app(LoginResponse::class);
});
}
/**
* Get the authentication pipeline instance.
*
* @param \Laravel\Fortify\Http\Requests\LoginRequest $request
* @return \Illuminate\Pipeline\Pipeline
*/
protected function loginPipeline(LoginRequest $request)
{
if (Fortify::$authenticateThroughCallback) {
return (new Pipeline(app()))->send($request)->through(array_filter(
call_user_func(Fortify::$authenticateThroughCallback, $request)
));
}
if (is_array(config('fortify.pipelines.login'))) {
return (new Pipeline(app()))->send($request)->through(array_filter(
config('fortify.pipelines.login')
));
}
return (new Pipeline(app()))->send($request)->through(array_filter([
config('fortify.limiters.login') ? null : EnsureLoginIsNotThrottled::class,
Features::enabled(Features::twoFactorAuthentication()) ? RedirectIfTwoFactorAuthenticatable::class : null,
AttemptToAuthenticate::class,
PrepareAuthenticatedSession::class,
]));
}
/**
* Destroy an authenticated session.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Request $request)
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}
2FA works as expected.
Breeze isn't meant to provide a fully functional authentication flow with 2FA. It's a basic simple starter kit with a login/registration flow. If you need 2FA you either need to build it in yourself like you did or use Jetstream: https://github.com/laravel/jetstream
I never said that Breeze should provide the functionality.
I think it's disingenuous for Fortify's 2FA documentation to be missing at least some mention that the developer will need to implement Fortify's pipeline to get 2FA to actually work.
Thanks @ChrisThompsonTLDR for posting your working controller - it helped me resolve why I wasn't able to get 2FA working. I agree that Fortify's documentation could be improved here.