LaravelでBreeze使ってるなら、簡単に2段階認証にできるぞ
最近は認証で2段階認証とか2要素認証ってほぼ必須ですよね。
Google Authenticatorを使って2要素認証を実装する記事は前回書きました。
Laravelに2要素認証を実装しよう
今回は、Breezeに標準で搭載されてるメール認証の機能(MustVerifyEmail)を使って、簡単に二段階認証を実装したいと思います。
通常ログイン→メールアドレスにログインコードを送付→認証フォームにコード入力→認証OK→ダッシュボードの流れになります。
現在の実装状況
前提条件として、LaravelにBreezeが実装されていること。
MustVerifyEmailが稼働していること。
MustVerifyEmailの稼働(Active化)は、Userクラスで行います。
クラス名にimplements MustVerifyEmail を追加します。
これでユーザー登録時にメール認証しないと登録できない仕組みができました。
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に追加していきます。
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が作成されます。
そちらでメール送信の内容を書きます。
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を作成し、メール本文のコードを書きます。
<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分間有効のコードとします。
ログイン認証コードを送付する間にユーザーはログアウトさせておきます。
※何らかの例外が発生するのを防ぐため
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の中に作成します。
<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に記載していきます。
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を拡張させて使えば、楽に構築できて便利ですね。
お疲れさまでした。
Discussion