Laravel, Breeze e Fortify — Parte 4: Implementando Autenticação com 2FA (Two-Factor Authentication)

2FA no Laravel com Fortify: Na Parte 3 definimos nosso projeto de exemplo, o Gerenciador de Tarefas, e nas anteriores configuramos a base do sistema. Agora, na sequência da nossa série sobre desenvolvimento web com a pilha Laravel 12 + Breeze + Fortify + Tailwind CSS + Alpine.js + Vite, chegamos a uma etapa crucial: implementar o Fortify e configurar a autenticação em dois fatores (2FA).

O Fortify é um backend para autenticação no Laravel que fornece, de forma desacoplada, funcionalidades robustas como:

  • Login e registro;
  • Confirmação de senha;
  • Recuperação e redefinição de senha;
  • Autenticação em dois fatores (2FA);
  • Verificação de e-mail (opcional no Laravel 12).
2FA no Laravel com Fortify

🎯 Por que usar Fortify junto com Breeze?

Relembrando, o Laravel Jetstream já inclui tudo isso, inclusive uma interface de autenticação robusta e 2FA, mas decidimos optar pelo Breeze por dois motivos:

  1. Interface mais simples e flexível, ideal para quem deseja customizar a aparência e comportamento das telas;
  2. Curva de aprendizado menor, com uma estrutura baseada em Blade e Tailwind, que facilita ajustes visuais e lógicos.

Com isso, usamos o Fortify apenas para a lógica de autenticação no backend e o Breeze para a interface frontend.


O que faremos nesta etapa:

  • Instalar e configurar o Laravel Fortify
  • Ativar e implementar a autenticação em dois fatores (2FA)
  • Realizar os ajustes necessários em Model, Controller, Views, Rotas e Middleware
  • Tudo isso integrado ao ambiente que configuramos nas partes anteriores

📦 Passo 1: Instalar o Laravel Fortify

No terminal, dentro do seu projeto Laravel, execute:

composer require laravel/fortify

Observação: Fortify não tem dependências de frontend, portanto não precisa rodar npm install ou npm run dev após a instalação. Ele é 100% backend.


🔧 Passo 2: Publicar as Configurações do Fortify

Execute o comando:

php artisan vendor:publish --provider="Laravel\Fortify\FortifyServiceProvider"

Isso irá gerar o arquivo de configuração e o provider para o Fortify:

config/fortify.php
app/Providers/FortifyServiceProvider.php
✔️ Observações Importantes no Laravel 12:
  • O Fortify já vem com o 2FA habilitado por padrão (suporte completo para 2FA), com as melhores práticas ativadas.
  • As rotas para o 2FA, login, registro, senha e perfil são fornecidas automaticamente pelo Fortify.
  • A verificação de e-mail é opcional (vem comentada por padrão no arquivo config/fortify.php).
  • O Laravel 12 utiliza auto-discovery de service providers, portanto não é mais necessário registrá-los manualmente no config/app.php.
🔍 Trecho do arquivo config/fortify.php:
'features' => [
    Features::registration(),
    Features::resetPasswords(),
    // Features::emailVerification(),
    Features::updateProfileInformation(),
    Features::updatePasswords(),
    Features::twoFactorAuthentication([
        'confirm' => true,
        'confirmPassword' => true,
        // 'window' => 0,
    ]),
],

👉 Dica: Mesmo que não use a confirmação de e-mail, descomente Features::emailVerification(). Em teoria não precisaria, na prática evita-se alguns erros de lógica no uso do Larevel Fortify.

Por padrão, o Fortify redireciona para /home. No nosso projeto, queremos redirecionar para /dashboard.

Ainda no arquivo config/fortify.php, altere:

'home' => '/dashboard',

🧠Passo 3 – Configurar o FortifyServiceProvider

O Laravel Fortify precisa de um Service Provider para definir quais views serão utilizadas no login, registro, redefinição de senha, entre outras funcionalidades. Para isso, no passo anterior, além de criar o config/fortify.php, criamos, de forma automática, o app/Providers/FortifyServiceProvider.php, que agora iremos configurar.

Edite esse arquivo com o seguinte conteúdo:

<?php

namespace App\Providers;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\ServiceProvider;
use Laravel\Fortify\Fortify;
use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use App\Actions\Fortify\UpdateUserPassword;
use App\Actions\Fortify\UpdateUserProfileInformation;
use Laravel\Fortify\Contracts\CreatesNewUsers;
use Laravel\Fortify\Contracts\ResetsUserPasswords;
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;

class FortifyServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
{
    $this->app->singleton(CreatesNewUsers::class, CreateNewUser::class);
    $this->app->singleton(UpdatesUserProfileInformation::class, UpdateUserProfileInformation::class);
    $this->app->singleton(UpdatesUserPasswords::class, UpdateUserPassword::class);
    $this->app->singleton(ResetsUserPasswords::class, ResetUserPassword::class);
}

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        // Definir as views personalizadas
        Fortify::loginView(fn () => view('auth.login'));

        Fortify::registerView(fn () => view('auth.register'));

        Fortify::requestPasswordResetLinkView(fn () => view('auth.forgot-password'));

        Fortify::resetPasswordView(fn ($request) => view('auth.reset-password', ['request' => $request]));

        Fortify::confirmPasswordView(fn () => view('auth.confirm-password'));

        Fortify::twoFactorChallengeView(fn () => view('auth.two-factor-challenge'));

        // A view de verificação de e-mail foi removida, pois não utilizamos
        // Caso queira usar no futuro, descomente:
        // Fortify::verifyEmailView(fn () => view('auth.verify-email'));

        // Definir manualmente a autenticação (opcional, mas recomendado)
        Fortify::authenticateUsing(function (Request $request) {
            $user = User::where('email', $request->email)->first();

            if (
                $user &&
                Hash::check($request->password, $user->password)
            ) {
                return $user;
            }

            return null;
        });
    }
}

Observação: Esse código pressupõe que você tenha as views correspondentes dentro da pasta:

resources/views/auth/

Por exemplo:

  • auth/login.blade.php
  • auth/register.blade.php
  • auth/forgot-password.blade.php
  • auth/reset-password.blade.php
  • auth/verify-email.blade.php
  • auth/two-factor-challenge.blade.php
  • auth/confirm-password.blade.php

Como estamos usando o Breeze, essas views já existem, com exceção de auth/two-factor-challenge.blade.php, que iremos criar.

Importante: Como, no passo anterior, não utilizamos o artisan para criar o provider, precisamos registrá-lo manualmente em bootstrap/providers.php.

<?php

return [
    App\Providers\AppServiceProvider::class,
    App\Providers\FortifyServiceProvider::class,
];

🧠 Passo 4 – Configurar o Rate Limiter (Novo no Laravel 12)

Agora, você precisa criar um Rate Limiter.

Para isso, vamos criar o RateLimiterServiceProvider com o Artisan.

No terminal, execute:

php artisan make:provider RateLimiterServiceProvider

Esse comando criará o arquivo app/Providers/RateLimiterServiceProvider.php.

Agora, edite esse arquivo com o seguinte conteúdo:

<?php

namespace App\Providers;

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Http\Request;
use Illuminate\Support\ServiceProvider;

class RateLimiterServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        //
    }

    public function boot(): void
    {
        RateLimiter::for('login', function (Request $request) {
            return Limit::perMinute(5)->by($request->email.$request->ip());
        });

        RateLimiter::for('two-factor', function (Request $request) {
            return Limit::perMinute(5)->by($request->session()->get('login.id'));
        });
    }
}

Diferente do app/Providers/FortifyServiceProvider.php, como usamos o artisan, esse novo provider já foi registrado no bootstrap/providers.php.


👤 Passo 5: Ajustar a Tabela “Users” e o Model “User”

Agora precisamos rodar as migrations para criar as tabelas necessárias para o 2FA. O Fortify adiciona colunas na tabela de usuários para armazenar os dados do 2FA:

php artisan migrate

Isso vai adicionar colunas como two_factor_secret, two_factor_recovery_codes, etc. na tabela users.

🔧 Observação:

Quando você executa php artisan migrate, o Fortify incluiu automaticamente suas migrations que adicionam as colunas necessárias para 2FA na tabela users existente.

Como funciona:

  • O Fortify vem com migrations pré-definidas
  • Essas migrations fazem ALTER TABLE na tabela users
  • Adicionam colunas como two_factor_secret, two_factor_recovery_codes, two_factor_confirmed_at

Vamos agora ajustar o Model User para o 2FA, no arquivo:

app/Models/User.php

Adicione o trait: TwoFactorAuthenticatable, que vai permitir que o modelo User trabalhe com as funcionalidades de 2FA.

use Laravel\Fortify\TwoFactorAuthenticatable;

class User extends Authenticatable
{
    use HasFactory, Notifiable, TwoFactorAuthenticatable;
    ...
}

Além disso, adicione os campos relacionados ao 2FA no array de $hidden e $casts (essas configurações no modelo User são para proteger os dados sensíveis do 2FA):

protected $hidden = [
    ...
    'two_factor_recovery_codes',
    'two_factor_secret',
];

protected $casts = [
    ...
    'two_factor_confirmed_at' => 'datetime',
];

app/Models/User.php completo:

<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Fortify\TwoFactorAuthenticatable;

class User extends Authenticatable
{
    /** @use HasFactory<\Database\Factories\UserFactory> */
    use HasFactory, Notifiable, TwoFactorAuthenticatable;

    /**
     * The attributes that are mass assignable.
     *
     * @var list<string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var list<string>
     */
    protected $hidden = [
        'password',
        'remember_token',
        'two_factor_recovery_codes',
        'two_factor_secret',
    ];

    /**
     * Get the attributes that should be cast.
     *
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
            'two_factor_confirmed_at' => 'datetime',
        ];
    }
}

🎨 Passo 6 — Criar as Views do 2FA: Gerenciamento e Desafio

Para que a Autenticação em Dois Fatores (2FA) funcione corretamente com o Laravel Fortify, precisamos criar duas views específicas. Cada uma possui um papel distinto no fluxo de autenticação.


🔹 6.1. View de Gerenciamento (two-factor.blade.php)

Por que criar?
Essa view é usada dentro da tela de perfil do usuário para permitir que ele ative, desative ou visualize o status da autenticação em dois fatores. Ela exibe o QR Code, códigos de recuperação e exige confirmação da senha.

Onde colocar:
resources/views/profile/two-factor.blade.php

O que essa view faz?

  • ✔️ Verifica se o 2FA está ativado ($enabled).
  • ✔️ Se está ativado:
    • Mostra o QR Code.
    • Exibe os códigos de recuperação.
    • Permite regenerar os códigos.
    • Permite desativar o 2FA.
  • ✔️ Se está desativado:
    • Oferece um botão para ativar o 2FA.
  • ✔️ Inclui um link para voltar à tela de edição de perfil.

Conteúdo:

<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('Two-Factor Authentication (2FA)') }}
        </h2>
    </x-slot>

    <div class="py-6">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">

            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">
                <h3 class="text-lg font-medium text-gray-900 mb-4">
                    {{ __('2FA Status:') }}
                </h3>

                {{-- Exibe uma mensagem de status se a 2FA foi habilitada e precisa de confirmação --}}
                @if (session('status') == 'two-factor-authentication-enabled')
                    <div class="mb-4 font-medium text-sm text-green-600">
                        {{ __('Two-factor authentication has been enabled. Please confirm by entering a code.') }}
                    </div>
                @endif

                {{-- Bloco para quando a autenticação de dois fatores está habilitada, mas ainda não confirmada --}}
                {{-- Verifica se a 2FA está habilitada ($enabled é true) E se a confirmação ainda não foi realizada (two_factor_confirmed_at é NULL) --}}
                @if ($enabled && is_null(Auth::user()->two_factor_confirmed_at))
                    <div class="text-yellow-600 font-semibold mb-4">
                        {{ __('Two-Factor Authentication is PENDING CONFIRMATION.') }}
                    </div>

                    <div class="mb-6">
                        <p class="mb-2">{{ __('Scan this QR Code with your authenticator app (Google Authenticator, Authy, etc.):') }}</p>
                        {{-- Exibe o QR Code SVG. O $qrCode é passado pelo ProfileController --}}
                        <div>{!! $qrCode !!}</div>
                    </div>

                    <p class="mb-4 text-sm text-gray-600">
                        {{ __('To finish enabling two factor authentication, scan the following QR code using your phone\'s authenticator application or enter the setup key and provide the generated OTP code.') }}
                    </p>

                    {{-- Formulário para o usuário inserir o código de autenticação e confirmar a 2FA --}}
                    {{-- Este formulário envia o código para a rota 'two-factor.confirm' do Fortify --}}
                    <form method="POST" action="{{ route('two-factor.confirm') }}">
                        @csrf
                        <div class="mt-4">
                            <label for="code" class="block font-medium text-sm text-gray-700">{{ __('Authentication Code') }}</label>
                            <input id="code" type="text" name="code" class="mt-1 block w-full" inputmode="numeric" autofocus autocomplete="one-time-code" />
                            {{-- Exibe erros de validação para o campo 'code' --}}
                            @error('code')
                                <span class="text-sm text-red-600">{{ $message }}</span>
                            @enderror
                        </div>

                        <div class="mt-4 flex items-center justify-end">
                            <x-primary-button>{{ __('Confirm') }}</x-primary-button>
                        </div>
                    </form>

                {{-- Bloco para quando a autenticação de dois fatores está habilitada E confirmada --}}
                {{-- Verifica se a 2FA está habilitada ($enabled é true) E se a confirmação JÁ foi realizada (two_factor_confirmed_at NÃO é NULL) --}}
                @elseif ($enabled && !is_null(Auth::user()->two_factor_confirmed_at))
                    <div class="text-green-600 font-semibold mb-4">
                        {{ __('Two-Factor Authentication is ENABLED.') }}
                    </div>

                    <div class="mb-6">
                        <p class="mb-2">{{ __('Scan this QR Code with your authenticator app (Google Authenticator, Authy, etc.):') }}</p>
                        {{-- Exibe o QR Code SVG. O $qrCode é passado pelo ProfileController --}}
                        <div>{!! $qrCode !!}</div>
                    </div>

                    <div class="mb-6">
                        <h4 class="font-semibold mb-2">{{ __('Recovery Codes:') }}</h4>
                        <ul class="list-disc pl-5">
                            {{-- Itera e exibe os códigos de recuperação. Os $recoveryCodes são passados pelo ProfileController --}}
                            @foreach ($recoveryCodes as $code)
                                <li>{{ $code }}</li>
                            @endforeach
                        </ul>

                        {{-- Formulário para regenerar os códigos de recuperação --}}
                        {{-- Ação para a rota 'two-factor.recovery-codes' --}}
                        <form method="POST" action="{{ route('two-factor.recovery-codes') }}" class="mt-4">
                            @csrf
                            <x-primary-button>{{ __('Regenerate Recovery Codes') }}</x-primary-button>
                        </form>
                    </div>

                    {{-- Formulário para desabilitar a autenticação de dois fatores --}}
                    {{-- Ação para a rota 'two-factor.disable' --}}
                    <form method="POST" action="{{ route('two-factor.disable') }}">
                        @csrf
                        @method('DELETE')
                        <x-danger-button>{{ __('Disable 2FA') }}</x-danger-button>
                    </form>

                {{-- Bloco para quando a autenticação de dois fatores está desabilitada --}}
                @else
                    <div class="text-red-600 font-semibold mb-4">
                        {{ __('Two-Factor Authentication is DISABLED.') }}
                    </div>

                    {{-- Formulário para habilitar a autenticação de dois fatores --}}
                    {{-- Ação para a rota 'two-factor.enable' --}}
                    <form method="POST" action="{{ route('two-factor.enable') }}">
                        @csrf
                        <x-primary-button>{{ __('Enable 2FA') }}</x-primary-button>
                    </form>
                @endif

                {{-- Botão para voltar para as configurações de perfil --}}
                <div class="mt-8">
                    <a href="{{ route('profile.edit') }}"
                       class="inline-flex items-center px-4 py-2 bg-gray-100 border border-gray-300
                              rounded-md font-semibold text-xs text-gray-700 uppercase tracking-widest
                              hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2
                              focus:ring-gray-500 transition">
                        {{ __('Back to Profile') }}
                    </a>
                </div>
            </div>
        </div>
    </div>
</x-app-layout>

Observação: Você deve incluir essa view como um item do menu de usuário, logo abaixo da opção “Profile/Perfil”.

Até lá, acesse como, por exemplo http://taskmanager.test/profile/two-factor. (Vide Passo Extra 2)


🔹 6.2. View do Desafio de Login (two-factor-challenge.blade.php)

Por que criar?
Depois que o usuário faz login com e-mail e senha, se o 2FA estiver habilitado, o Fortify redireciona para essa view, que solicita o código gerado pelo aplicativo autenticador.

Onde colocar:
resources/views/auth/two-factor-challenge.blade.php

Conteúdo:

<x-guest-layout>
    <div class="mb-4 text-sm text-gray-600">
        {{ __('Please confirm access to your account by entering the authentication code provided by your authenticator application.') }}
    </div>

    @if (session('status'))
        <div class="mb-4 font-medium text-sm text-green-600">
            {{ session('status') }}
        </div>
    @endif

    <form method="POST" action="{{ route('two-factor.login') }}">
        @csrf

        <!-- Authentication Code -->
        <div>
            <label for="code" class="block font-medium text-sm text-gray-700">{{ __('Authentication Code') }}</label>
            <input id="code" class="block mt-1 w-full border-gray-300 rounded-md shadow-sm" type="text" name="code" autofocus autocomplete="one-time-code" />
            @error('code')
                <div class="text-red-500 text-sm">{{ $message }}</div>
            @enderror
        </div>

        <!-- Submit Button -->
        <div class="flex items-center justify-center mt-4">
            <button
                type="submit"
                class="inline-flex items-center px-4 py-2 bg-blue-600 border border-transparent rounded-md font-semibold text-sm text-white uppercase tracking-widest hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
            >
                {{ __('Verify') }}
            </button>
        </div>
    </form>
</x-guest-layout>

🛠️ Registrando a View no Fortify

Garanta que a view do desafio (two-factor-challenge) esteja registrada no seu FortifyServiceProvider (vide Passo 3):

Fortify::twoFactorChallengeView(fn () => view('auth.two-factor-challenge'));

Com essas duas views, o sistema estará quase preparado para oferecer uma experiência completa de autenticação em dois fatores, tanto no gerenciamento pelo usuário quanto na proteção do login.


🧩Passo 7 — Organizar as Rotas de Autenticação e Perfil

Neste passo, vamos organizar os arquivos de rota routes/auth.php e routes/web.php para garantir compatibilidade com o Fortify no Laravel 12, evitar duplicações e assegurar que o gerenciamento de perfil e autenticação em dois fatores (2FA) funcione corretamente, sem a necessidade de alterar a view edit.blade.php do Breeze.

Contexto do Projeto

  • Laravel 12 com Breeze + Fortify.
  • As views de login, registro, recuperação de senha e 2FA são definidas diretamente no FortifyServiceProvider.php.
  • O Fortify já registra automaticamente as rotas para autenticação.
  • O objetivo é centralizar as rotas da aplicação e permitir acesso direto à página de gerenciamento de 2FA via menu.

Ao integrar o Laravel Breeze, é comum que ele gere o arquivo routes/auth.php contendo diversas rotas de autenticação (como login, registro, etc.). No entanto, como estamos utilizando o Fortify como backend de autenticação e configuramos um FortifyServiceProvider específico para definir as views e o comportamento desses processos, o Fortify já se encarrega de registrar suas próprias rotas internas para essas funcionalidades.

Ter as mesmas rotas declaradas tanto em routes/auth.php quanto implicitamente pelo Fortify (e às vezes explicitamente em routes/web.php) resulta em duplicação desnecessária.

Embora o Laravel geralmente resolva escolhendo a primeira rota que corresponde, essa redundância pode levar a confusões, dificultar a manutenção e, em cenários mais complexos, gerar erros de lógica ou comportamento inesperado, tornando a limpeza e centralização dessas rotas uma prática recomendada.


7.1. Limpar o arquivo routes/auth.php

Este arquivo deve ser minimalista, já que o Fortify já lida internamente com login, registro e autenticação. Manter rotas duplicadas pode causar conflitos ou confusão.

🛠️ Ação:

Remover rotas de /profile ou quaisquer duplicações de rotas já tratadas no FortifyServiceProvider.

📄 routes/auth.php:

<?php

use Illuminate\Support\Facades\Route;

Route::middleware('auth')->group(function () {
    // Todas as rotas de perfil duplicadas foram removidas daqui.
    // Este arquivo agora deve estar vazio neste grupo ou conter apenas rotas muito específicas
    // de autenticação não gerenciadas pelo Fortify.
    // Este arquivo pode ficar minimalista/vazio.
});

💡 Se você deseja criar endpoints personalizados não gerenciados pelo Fortify, poderá adicioná-los aqui no futuro.


7.2. Ajustar o arquivo routes/web.php

Este é o local apropriado para definir as rotas da aplicação após o login, incluindo dashboard, perfil e a nova tela de gerenciamento de 2FA.

🛠️ Ações:

  • Mantenha as rotas de / e /dashboard.
  • Centralize as rotas de /profile aqui, dentro de auth e verified.
  • Inclua a nova rota dedicada para a tela de 2FA: /profile/two-factor.

📄 routes/web.php:

<?php

use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

Route::get('/dashboard', function () {
    return view('dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');

Route::middleware(['auth', 'verified'])->group(function () {
    // Perfil do usuário
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');

    // Gerenciamento de 2FA
    Route::get('/profile/two-factor', [ProfileController::class, 'showTwoFactorForm'])
        ->name('profile.two-factor');
});

require __DIR__.'/auth.php';

💡 Separar a tela de gerenciamento de 2FA em uma rota própria permite exibi-la como uma página autônoma acessada diretamente por menu ou link.


7.3. Verificar o FortifyServiceProvider.php

Certifique-se de que suas views estão corretamente registradas para que o Fortify saiba qual tela exibir em cada fluxo.

🛠️ Ação:

Verifique se as definições de view estão presentes no método boot(). – já realizado n o Passo 3.

📄 Trecho de Exemplo — FortifyServiceProvider.php:

// Definir as views personalizadas
Fortify::loginView(fn () => view('auth.login'));

Fortify::registerView(fn () => view('auth.register'));

Resumo Final da Organização das Rotas

ArquivoResponsabilidade
FortifyServiceProviderDefine as views usadas pelo Fortify.
routes/auth.phpMínimo ou vazio. Usado para rotas de autenticação muito específicas.
routes/web.phpContém as rotas principais da aplicação (dashboard, perfil, gerenciamento 2FA).

Com essa organização, o sistema de rotas estará enxuto, bem estruturado, sem conflitos e totalmente compatível com o Laravel 12 + Breeze + Fortify.


🧩 Passo 8 — Ajustar o ProfileController para Suporte ao 2FA

Agora que organizamos corretamente as rotas e criamos uma view dedicada para a página de gerenciamento da autenticação em dois fatores (2FA), precisamos ajustar o ProfileController para alimentar essa página com os dados corretos.

Sem isso, a view profile.two-factor.blade.php não terá acesso ao estado atual do 2FA (habilitado ou não), ao QR Code para ativação e aos códigos de recuperação.

🎯 Objetivo deste passo

Criar um método no ProfileController chamado showTwoFactorForm, que será responsável por:

  • Verificar se o 2FA está habilitado para o usuário logado.
  • Gerar o QR Code de ativação do 2FA.
  • Obter os códigos de recuperação.
  • Enviar esses dados para a view de gerenciamento do 2FA.

✍️ Implementação no ProfileController.php

Abra o arquivo app/Http/Controllers/ProfileController.php e adicione o seguinte método:

/**
 * Exibir a tela de gerenciamento de autenticação em dois fatores (2FA).
 */
public function showTwoFactorForm(Request $request): \Illuminate\View\View
{
    $user = $request->user();

    // Inicialize as variáveis com valores padrão nulos/vazios
    $qrCode = null;
    $recoveryCodes = [];

    // SOMENTE gere o QR Code e os códigos de recuperação SE o 2FA estiver ativado
    if (! is_null($user->two_factor_secret)) {
        $qrCode = $user->twoFactorQrCodeSvg();
        $recoveryCodes = json_decode(decrypt($user->two_factor_recovery_codes), true); // Decrypt e decode
    }

    return view('profile.two-factor', [
        'user' => $user,
        'enabled' => !is_null($user->two_factor_secret),
        'qrCode' => $qrCode, // Agora pode ser null se 2FA estiver desabilitado
        'recoveryCodes' => $recoveryCodes, // Agora pode ser vazio se 2FA estiver desabilitado
    ]);
}

🔐 Este método utiliza os recursos internos do Fortify (já disponíveis no modelo User) para recuperar todos os dados necessários à tela de gerenciamento do 2FA.


🧠 Explicando o que cada item faz:

  • two_factor_secret: campo armazenado no banco que indica se o 2FA está ativo.
  • twoFactorQrCodeSvg(): método do Fortify que gera o código QR no formato SVG.
  • two_factor_recovery_codes: são os códigos de backup criptografados.
  • decrypt() + json_decode(): decodificam esses códigos para serem exibidos na view.

✅ Resultado esperado

Com este método implementado, ao acessar a rota /profile/two-factor (já criada no web.php), o usuário verá uma interface funcional para:

  • Ativar ou desativar o 2FA.
  • Escanear o QR Code com um app autenticador (como Google Authenticator ou Authy).
  • Copiar seus códigos de recuperação.

🔥 Esclarecimento Importante:

O Laravel Fortify já fornece as rotas responsáveis por ativar, desativar e gerenciar o 2FA no backend. Contudo, para oferecer uma interface onde o usuário possa visualizar o status do 2FA (ativado ou não), acessar o QR Code e seus códigos de recuperação, foi preciso criar um método no nosso ProfileController.

Nota: O Laravel Fortify cria automaticamente rotas para as ações do 2FA, como:

  • Ativar 2FA → /user/two-factor-authentication (POST)
  • Desativar 2FA → /user/two-factor-authentication (DELETE)
  • Gerar códigos de recuperação → /user/two-factor-recovery-codes (POST)
  • Desafio no login (quando 2FA está ativado) → /two-factor-challenge (GET / POST)

Essas rotas são internas do Fortify e operam via APIs ou requests HTTP, sem exibir telas.

➡️ O Fortify NÃO cria uma rota para uma página de gerenciamento do 2FA, como /profile/two-factor. Essa é uma responsabilidade do desenvolvedor criar, se desejar uma interface visual.


📌 Conclusão do Passo 8

Esse ajuste finaliza a integração da funcionalidade 2FA no seu projeto Laravel 12 com Breeze e Fortify. O sistema agora está pronto para oferecer uma camada extra de segurança aos usuários, com interface clara, rotas organizadas e controle total sobre o processo de autenticação em dois fatores.


🎨 Passo Extra 1: Melhorar a UX

Para garantir uma experiência mais fluida e consistente no uso da aplicação, também iremos realizar uma pequena melhoria na tela de confirmação de senha (Confirm Password). Por padrão, essa tela é exibida em um layout isolado, que ocupa toda a área da janela, sem cabeçalho ou menu. Isso quebra a harmonia visual da interface, fazendo com que o usuário tenha a impressão de estar fora do sistema. Por isso, altere:

resources\views\auth\confirm-password.blade.php
<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('Confirm Password') }}
        </h2>
    </x-slot>

    <div class="py-6">
        <div class="max-w-md mx-auto sm:px-6 lg:px-8">

            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">

                <p class="mb-4 text-sm text-gray-600">
                    {{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
                </p>

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

                    <!-- Password -->
                    <div class="mb-4">
                        <label for="password" class="block font-medium text-sm text-gray-700">
                            {{ __('Password') }}
                        </label>

                        <input id="password" 
                               type="password" 
                               name="password" 
                               required 
                               autocomplete="current-password"
                               class="mt-1 block max-w-xs rounded-md shadow-sm border-gray-300 
                                      focus:border-blue-500 focus:ring focus:ring-blue-500 focus:ring-opacity-50" />

                        @error('password')
                            <span class="text-sm text-red-600">{{ $message }}</span>
                        @enderror
                    </div>

                    <div class="flex justify-start">
                        <x-primary-button>
                            {{ __('Confirm') }}
                        </x-primary-button>
                    </div>
                </form>

            </div>
        </div>
    </div>
</x-app-layout>

🎨 Passo Extra 2: Colocar o 2FA no Menu de Usuário

Com a rota profile.two-factor configurada e funcionando como uma página independente, o próximo passo é torná-la acessível para o usuário. O local mais intuitivo é no menu de usuário do Breeze, geralmente encontrado no canto superior direito após o login.

1. Localize o arquivo de navegação: Abra o arquivo resources/views/layouts/navigation.blade.php. Este arquivo é responsável por renderizar a barra de navegação principal, incluindo o menu drop-down do usuário autenticado.

2. Encontre a seção do menu drop-down do usuário: Dentro de navigation.blade.php, localize o bloco de código que define os links do menu drop-down do usuário. Ele geralmente contém o link “Profile” e o formulário de “Log Out”. O código deve ser semelhante a este:

<x-slot name="content">
    <x-dropdown-link :href="route('profile.edit')">
        {{ __('Profile') }}
    </x-dropdown-link>
    ....

3. Adicione o link para 2FA: Insira um novo x-dropdown-link para a rota profile.two-factor onde você achar mais adequado dentro desse menu, idealmente abaixo do link “Profile”:

<x-dropdown-link :href="route('profile.two-factor')">
    {{ __('2FA') }}
</x-dropdown-link>

Após realizar essa modificação e salvar o arquivo, recarregue seu aplicativo no navegador. Ao abrir o menu do usuário (geralmente clicando no nome do usuário logado), você verá a nova opção “2FA” que direcionará o usuário para a página de gerenciamento de 2FA.


🔐 Passo Extra 3: Como Funciona o Processo do 2FA

  1. O usuário ativa o 2FA na tela de perfil.
  2. Um QR Code é gerado.
  3. O QR Code é lido por um app autenticador (Google Authenticator, Microsoft Authenticator, Authy, etc.).
  4. Na próxima tentativa de login, após informar e-mail e senha, o usuário é direcionado para a tela do desafio do 2FA (resources/views/auth/two-factor-challenge.blade.php).
  5. Nessa tela, ele informa o código gerado no aplicativo autenticador.

Se não tiver acesso ao app, pode usar um dos códigos de recuperação.


📜 Observação Sobre o Frontend

  • O Fortify não oferece frontend nativo, por isso construímos manualmente a interface com Blade + Tailwind CSS + Alpine.js. (O Fortify é um backend de autenticação puro. Ele fornece a lógica de autenticação via contratos e ações, mas a interface do usuário (views) é de responsabilidade do desenvolvedor. A integração com Blade, Tailwind CSS e Alpine.js é a forma comum de fazer isso, especialmente com o Breeze.)
  • Não há necessidade de rodar comandos como npm install especificamente para o Fortify, exceto se desejar estilizar as novas views ou alterar arquivos CSS/JS existentes. (npm install é para dependências de frontend (JavaScript, CSS, frameworks como React/Vue, ou utilitários como Tailwind CSS). O Fortify em si é uma dependência PHP e não tem suas próprias dependências de frontend que exigiriam npm install. Você só precisaria do npm install e npm run dev se estivesse usando o Breeze (que inclui o Tailwind CSS e Alpine.js) e alterando os arquivos CSS/JS do seu projeto. As views que você criou para o Fortify usam esses assets, então se você alterar as views ou estilos, precisará recompilar os assets.)

🎯 Conclusão

Agora nossa aplicação possui uma autenticação robusta, protegida por 2FA (autenticação em dois fatores), elevando consideravelmente o nível de segurança do sistema.

Na Parte 5, vamos avançar para a construção do Dashboard e do CRUD de tarefas, implementando as funcionalidades principais do nosso Gerenciador de Tarefas.

🔗 Leitura recomendada: Desenvolvimento Web com Laravel, Breeze, Fortify e Tailwind — Parte 3: Apresentando o Projeto Exemplo.