laravel / fortify

Backend controllers and scaffolding for Laravel authentication.

Home Page:https://laravel.com/docs/fortify

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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

  1. composer create-project laravel/laravel ./
  2. update .env to have database access

Install Fortify

  1. composer require laravel/fortify
  2. php artisan vendor:publish --provider="Laravel\Fortify\FortifyServiceProvider"
  3. php artisan migrate
  4. Follow the Two Factor Authentication steps.

Install Breeze

I know this isn't required, but it helps test the bug.

  1. composer require laravel/breeze --dev
  2. php artisan breeze:install
  3. npm install
  4. npm run dev
  5. php artisan migrate

Test

  1. visit the site
  2. register a user
  3. activate 2fa
  4. logout
  5. 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.