Zenn
📧

LaravelでBreeze使ってるなら、簡単に2段階認証にできるぞ

2025/03/16に公開
1

最近は認証で2段階認証とか2要素認証ってほぼ必須ですよね。
Google Authenticatorを使って2要素認証を実装する記事は前回書きました。
Laravelに2要素認証を実装しよう

今回は、Breezeに標準で搭載されてるメール認証の機能(MustVerifyEmail)を使って、簡単に二段階認証を実装したいと思います。
通常ログイン→メールアドレスにログインコードを送付→認証フォームにコード入力→認証OK→ダッシュボードの流れになります。

現在の実装状況

前提条件として、LaravelにBreezeが実装されていること。
MustVerifyEmailが稼働していること。

MustVerifyEmailの稼働(Active化)は、Userクラスで行います。
クラス名にimplements MustVerifyEmail を追加します。
これでユーザー登録時にメール認証しないと登録できない仕組みができました。

models/User.php
namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;

class User extends Authenticatable implements MustVerifyEmail
{

}

controllerの作成

次はログイン時の2段階認証専用のコントローラを作成します。
TwoFactorControllerをapp/Http/Controllers/Authの中に作成します。

php artisan make:controller Auth/TwoFactorController

ここでログイン後のコード認証の処理を行います。

routes.php に認証用のルートを追加

TwoFactorAuthControllerで行う処理のためのルートを設定します。
①ログイン認証コードを記入するフォームを表示するルート
②フォームを送信して認証処理を行うルート
この2つをauth.phpに追加していきます。

routes/auth.php
use App\Http\Controllers\Auth\TwoFactorController;

Route::middleware('guest')->group(function () {
    //... 略
    Route::post('login', [AuthenticatedSessionController::class, 'store']);

    // 認証用ページ
    Route::controller(TwoFactorController::class)->group(function () {
        Route::get('two-factor/verify', 'showVerifyForm')->name('two-factor.verify');
        Route::post('two-factor/verify', 'verifyTwoFactorCode')->name('two-factor.verify.post');
    });
    
});

ログイン認証コードをメール送信するメールクラスの作成

ログインフォームでメールとパスワードで認証した後に、ログイン認証コードをメールで送信するために、メールクラスを作成します。

php artisan make:mail TwoFactorCode

これでapp\Mailの中にTwoFactorCode.phpが作成されます。
そちらでメール送信の内容を書きます。

app\Mail\TwoFactorCode.php
class TwoFactorCode extends Mailable
{
    use Queueable, SerializesModels;

    public $code;

    /**
     * Create a new message instance.
     */
    public function __construct($code)
    {
        $this->code = $code;
    }

    /**
     * Get the message envelope.
     */
    public function envelope(): Envelope
    {
        return new Envelope(
            subject: 'Login Verification Code',
        );
    }

    /**
     * Get the message content definition.
     */
    public function content(): Content
    {
        return new Content(
            view: 'mail.two-factor',
        );
    }
}

ついでにapp\resources\views\mail に two-factor.blade.phpを作成し、メール本文のコードを書きます。

mail\two-factor.blade.php
<div>
    <h1>ログイン認証コード</h1>
    <p>以下の6桁のコードを入力して、ログインを完了してください:</p>
    <h2 style="font-size: 24px; letter-spacing: 5px; text-align: center; padding: 10px; background: #f3f4f6; border-radius: 4px;">{{ $code }}</h2>
    <p>このコードは10分間有効です。</p>
</div>

既存のLogin処理の変更

元々の処理はAuth\AuthenticatedSessionControllerのstoreにあります。
このstoreの部分を少し修正します。
ログイン認証コードの生成は、10分間有効のコードとします。
ログイン認証コードを送付する間にユーザーはログアウトさせておきます。
※何らかの例外が発生するのを防ぐため

Auth\AuthenticatedSessionController
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Auth;
use App\Mail\TwoFactorCode;

public function store(LoginRequest $request): RedirectResponse
{
    $request->authenticate();
    //$request->session()->regenerate();
    //return redirect()->intended(route('dashboard', absolute: false));

    /*
    * 二段階認証の有効化を行う
    */
    $user_id = Auth::id();

    // 一旦ログアウト
    Auth::logout();

    // 2段階認証コードを生成
    $code = mt_rand(100000, 999999);

    // セッションに保存(10分間の有効期限とする)
    $request->session()->put('two_factor_code', [
        'code' => Hash::make($code),
        'user_id' => $user_id,
        'expires_at' => now()->addMinutes(10),
        'remember' => $request->boolean('remember')
    ]);
    
    // メールを送信
    $user = User::find($user_id);
    Mail::to($user->email)->send(new TwoFactorCode($code));
    
    return redirect()->route('two-factor.verify');
}   

これで、メールアドレスとパスワードのログインが成功したら、ログイン認証コードがユーザーのメールアドレスに送信され、ログイン認証フォームが表示されるようになります。

ログイン認証フォームの作成

ログイン認証フォームのルートは作成済みなので、ログイン認証を表示するビューをviews\authの中に作成します。

auth\two-factor.blade.php
<x-guest-layout>
    <x-auth-card>
        <x-slot name="logo">
            <a href="/">
                <x-application-logo class="w-20 h-20 fill-current text-gray-500" />
            </a>
        </x-slot>

        <div class="mb-4 text-sm text-gray-600">
            認証コードをメールで送信しました。届いたコードを入力してログインを完了してください。
        </div>

        @if ($errors->any())
        <div>
            <div class="font-medium text-red-600">
            {{ __('エラーが発生しました。入力内容を確認してください。') }}
            </div>

            <ul class="mt-3 list-disc list-inside text-sm text-red-600">
                @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
                @endforeach
            </ul>
        </div>
        @endif

        <form method="POST" action="{{ route('two-factor.verify.post') }}">
            @csrf
            <!-- Verification Code -->
            <div>
                <x-label for="code" value="認証コード" />
                <x-input id="code" class="block mt-1 w-full" type="text" name="code" :value="old('code')" required autofocus />
            </div>

            <div class="flex items-center justify-end mt-4">
                <x-button>
                   認証する
                </x-button>
            </div>
        </form>
    </x-auth-card>
</x-guest-layout>

既存のauth\login.blade.phpのデザインを流用しているので、適時変更入れてください。

TwoFactorController の処理を作成

ここからはログイン認証コードの処理の実装になります。
先ほどHttp\Authの中に作ったTwoFactorControllerに記載していきます。

Auth\TwoFactorController.php
namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;

class TwoFactorController extends Controller
{
    // 2段階認証の確認画面
    public function showVerifyForm()
    {
        if (!session()->has('two_factor_code')) {
            return redirect()->route('login');
        }

        return view('auth.two-factor');
    }

    // 2段階認証の検証
    public function verifyTwoFactorCode(Request $request)
    {
        $request->validate([
            'code' => ['required', 'string', 'numeric', 'digits:6'],
        ]);

        $twoFactorCode = session('two_factor_code');

        if (!$twoFactorCode || now()->isAfter(twoFactorCode['expires_at'])) {
            session()->forget('two_factor_code');
            return redirect()->route('login')
                ->withErrors(['code' => '2要素認証セッションの有効期限が切れました。再度ログインしてください。']);
        }

        if (!Hash::check($request->code, $twoFactorCode['code'])) {
            return back()->withErrors(['code' => '入力されたコードが正しくありません。']);
        }

        // 認証成功
        $user = User::findOrFail($twoFactorCode['user_id']);

        // ユーザーログイン
        Auth::login($user, $twoFactorCode['remember']);

        // セッション再生成
        $request->session()->regenerate();

        // セッションデータを削除
        session()->forget('two_factor_code');

        return redirect()->intended(route('user.dashboard', absolute: false));
    }

}

これでログイン認証のコードが、送付したものと入力したものが一致すればダッシュボードが表示されログインが成功します。

Breezeにもともと実装されているMustVerifyEmailを拡張させて使えば、楽に構築できて便利ですね。
お疲れさまでした。

1

Discussion

ログインするとコメントできます