Laravel 11 Fortify 2FA

# Laravel 11 での Fortify を使用した2FA実装の手順
- bootstrap 5
- laravel 11
- 参考
    - https://laravel.com/docs/11.x/fortify
    - https://laravel.com/docs/11.x/authentication
    - https://readouble.com/laravel/11.x/ja/fortify.html
    - https://readouble.com/laravel/11.x/ja/authentication.html

0. 2FA設定の入り口となる resources/views/home.blade.php の設置

```html

@extends('layouts.app')

@section('title', 'ホーム')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">ダッシュボード</div>

                <div class="card-body">
                    @if (session('status'))
                        <div class="alert alert-success" role="alert">
                            {{ session('status') }}
                        </div>
                    @endif

                    <p>ログインしました！</p>

                    <hr>

                    <h5>アカウント設定</h5>
                    @if (session('status') != 'two-factor-authentication-enabled')
                        <div class="mb-4 font-medium text-sm">
                            以下の、２要素認証の設定を完了して下さい。
                            <ul class="list-unstyled">
                                <li><a href="{{ route('two-factor.index') }}" class="btn btn-primary mt-2">2段階認証設定</a></li>
                            </ul>
                        </div>
                    @endif

                    @if (session('status') == 'two-factor-authentication-confirmed')
                        <div class="mb-4 font-medium text-sm">
                            ２要素認証を確認し、有効にしました。
                        </div>
                    @endif
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

```

1. Fortify の設定 (`config/fortify.php`):

```php
<?php

use Laravel\Fortify\Features;

return [
    'features' => [
        Features::registration(),
        Features::resetPasswords(),
        Features::emailVerification(),
        Features::updateProfileInformation(),
        Features::updatePasswords(),
        Features::twoFactorAuthentication([
            'confirmPassword' => true,
        ]),
    ],
];

```

2. User モデルの更新 (`app/Models/User.php`):

```php
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Fortify\TwoFactorAuthenticatable;

class User extends Authenticatable
{
    use TwoFactorAuthenticatable;

    // ...
}

```

3. 2FA管理ビューの作成 (`resources/views/auth/two-factor-auth.blade.php`):

```html
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Two Factor Authentication') }}</div>

                <div class="card-body">
                    @if (session('status') == 'two-factor-authentication-enabled')
                        <div class="alert alert-success" role="alert">
                            {{ __('Two factor authentication has been enabled.') }}
                        </div>
                    @endif

                    @if (session('status') == 'two-factor-authentication-confirmed')
                        <div class="mb-4 font-medium text-sm alert alert-success">
                            {{ __('Two factor authentication confirmed and enabled successfully.') }}
                        </div>
                    @endif

                    @if (auth()->user()->two_factor_secret)
                        @if (!auth()->user()->two_factor_confirmed_at)
                            <div class="mb-4">
                                {{ __('Two factor authentication is now enabled. Scan the following QR code using your phone\'s authenticator application or enter the setup key.') }}
                            </div>

                            <div class="mb-4">
                                {!! auth()->user()->twoFactorQrCodeSvg() !!}
                            </div>

                            <div class="mb-4">
                                <p>{{ __('Setup Key') }}: {{ decrypt(auth()->user()->two_factor_secret) }}</p>
                            </div>

                            <div class="mb-4">
                                <h3>{{ __('Recovery Codes') }}</h3>
                                <p>{{ __('Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.') }}</p>
                            
                                <div class="bg-light p-3 mb-4">
                                    @foreach (json_decode(decrypt(auth()->user()->two_factor_recovery_codes), true) as $code)
                                        <div>{{ $code }}</div>
                                    @endforeach
                                </div>
                            </div>

                            <form method="POST" action="{{ url('user/confirmed-two-factor-authentication') }}">
                                @csrf
                                <div class="mb-3">
                                    <label for="code" class="form-label">{{ __('Verification Code') }}</label>
                                    <input id="code" type="text" class="form-control" name="code" required autofocus>
                                </div>
                                <button type="submit" class="btn btn-primary">
                                    {{ __('Confirm & Enable Two-Factor') }}
                                </button>
                            </form>
                        @else
                            <div class="mb-4">
                                {{ __('Two factor authentication is enabled and confirmed.') }}
                            </div>

                            <div class="mt-4">
                                <form method="POST" action="{{ url('user/two-factor-authentication') }}">
                                    @csrf
                                    @method('DELETE')
                                    <button type="submit" class="btn btn-danger">
                                        {{ __('Disable Two-Factor') }}
                                    </button>
                                </form>
                            </div>
                        @endif
                    @else
                        <form method="POST" action="{{ url('user/two-factor-authentication') }}">
                            @csrf
                            <button type="submit" class="btn btn-primary">
                                {{ __('Enable Two-Factor') }}
                            </button>
                        </form>
                    @endif
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

```

4. 2FA挑戦ビューの作成 (`resources/views/auth/two-factor-challenge.blade.php`):

```html
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Two Factor Challenge') }}</div>

                <div class="card-body">
                    <form method="POST" action="{{ url('/two-factor-challenge') }}">
                        @csrf

                        <div class="mb-3">
                            {{ __('Please enter your authentication code to login.') }}
                        </div>

                        <div class="mb-3">
                            <label for="code" class="form-label">{{ __('Authentication Code') }}</label>
                            <input id="code" type="text" class="form-control @error('code') is-invalid @enderror" name="code" required autocomplete="current-password">
                            @error('code')
                                <span class="invalid-feedback" role="alert">
                                    <strong>{{ $message }}</strong>
                                </span>
                            @enderror
                        </div>

                        <div class="mb-3">
                            <button type="submit" class="btn btn-primary">
                                {{ __('Submit') }}
                            </button>
                        </div>
                    </form>

                    <hr>

                    <form method="POST" action="{{ url('/two-factor-challenge') }}">
                        @csrf

                        <div class="mb-3">
                            {{ __('If you lost your two factor authentication device, you may use a recovery code to login.') }}
                        </div>

                        <div class="mb-3">
                            <label for="recovery_code" class="form-label">{{ __('Recovery Code') }}</label>
                            <input id="recovery_code" type="text" class="form-control @error('recovery_code') is-invalid @enderror" name="recovery_code" autocomplete="current-password">
                            @error('recovery_code')
                                <span class="invalid-feedback" role="alert">
                                    <strong>{{ $message }}</strong>
                                </span>
                            @enderror
                        </div>

                        <div class="mb-3">
                            <button type="submit" class="btn btn-secondary">
                                {{ __('Use Recovery Code') }}
                            </button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

```

5. パスワード確認ビューの作成 (`resources/views/auth/confirm-password.blade.php`):

```html
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Confirm Password') }}</div>

                <div class="card-body">
                    {{ __('Please confirm your password before continuing.') }}

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

                        <div class="mb-3">
                            <label for="password" class="form-label">{{ __('Password') }}</label>
                            <input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="current-password">

                            @error('password')
                                <span class="invalid-feedback" role="alert">
                                    <strong>{{ $message }}</strong>
                                </span>
                            @enderror
                        </div>

                        <div class="mb-0">
                            <button type="submit" class="btn btn-primary">
                                {{ __('Confirm Password') }}
                            </button>

                            @if (Route::has('password.request'))
                                <a class="btn btn-link" href="{{ route('password.request') }}">
                                    {{ __('Forgot Your Password?') }}
                                </a>
                            @endif
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

```

6. ルートの設定 (`routes/web.php`):

```php
<?php

use Illuminate\Support\Facades\Route;

// 他のルート...

Route::view('/user/two-factor-authentication', 'auth.two-factor-auth')
    ->middleware(['auth', 'password.confirm']);

```

7. FortifyServiceProvider の更新 (`app/Providers/FortifyServiceProvider.php`):

```php
<?php

namespace App\Providers;

use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use App\Actions\Fortify\UpdateUserPassword;
use App\Actions\Fortify\UpdateUserProfileInformation;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Laravel\Fortify\Fortify;

class FortifyServiceProvider extends ServiceProvider
{
    public function register()
    {
        //
    }

    public function boot()
    {
        Fortify::createUsersUsing(CreateNewUser::class);
        Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
        Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
        Fortify::resetUserPasswordsUsing(ResetUserPassword::class);

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

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

        Fortify::loginView(function () {
            return view('auth.login');
        });

        Fortify::registerView(function () {
            return view('auth.register');
        });

        Fortify::requestPasswordResetLinkView(function () {
            return view('auth.forgot-password');
        });

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

        Fortify::verifyEmailView(function () {
            return view('auth.verify-email');
        });

        Fortify::twoFactorChallengeView(function () {
            return view('auth.two-factor-challenge');
        });

        Fortify::confirmPasswordView(function () {
            return view('auth.confirm-password');
        });
    }
}

```

8. アプリケーションの設定 (`config/app.php`):

```php
<?php

return [
    // ...

    'providers' => [
        // ...
        App\Providers\FortifyServiceProvider::class,
    ],

    // ...
];

```

9. データベースマイグレーションの実行（既に実行済みでない場合）:

```bash
php artisan migrate
```

これらの手順を実行することで、Laravel 11 に Fortify を使用した完全な2FA機能が実装されます。この実装には以下の機能が含まれます：

- 2FAの有効化と無効化
- QRコード、セットアップキー、リカバリーコードの同時表示
- 2FA挑戦（ログイン時の認証）
- リカバリーコードの使用
- パスワード確認（セキュリティが重要な操作の前）

実装後は、各機能が正しく動作することを確認するためのテストを行うことをお勧めします。特に、2FAの有効化、確認、ログインプロセス、回復コードの使用、およびパスワード確認機能をテストしてください。

セキュリティ上の注意点：
- QRコード、セットアップキー、リカバリーコードは非常に機密性の高い情報です。ユーザーにこれらの情報を安全に保管するよう強く促してください。
- アプリケーションのログにこれらの機密情報が記録されないよう注意してください。

この実装により、セキュアで使いやすい2FA機能がアプリケーションに追加されます。必要に応じて、ビューのスタイリングやテキストをカスタマイズして、アプリケーションの全体的なデザインに合わせることができます。