Laravel FortifyによるJetstreamを使わない2要素認証

Laravel8から認証基盤としてJetstreamが登場しました。それ以外にもLaravel BreezeというJetstreamをすっきりシンプルにしたものもあります。

つまり認証基盤をスキャフォールド的にパパっと構築したい場合はJetstreamかBreezeの2択になりますが、Jetstreamが2要素認証に対応しているのに対して、Breezeは2要素認証に対応していないようです。

つまり2要素認証を組み上げたい場合はJetstreamを使うしかないのですが、JetstreamはフロントエンドをLivewireかInertiaで構築する必要があります。

今回はフロントエンドをただのBladeで用意したいorフロントエンドを分けてAPIを叩きたいという目的のために、2要素認証をFortifyで構築していく方法をご紹介します。

Fortifyとは

JetstreamとBreezeがフロントエンドやルートなどをひっくるめてスキャフォールドとして認証基盤をコマンド1つで構築してくれるのに対して、Fortifyが提供するのはあくまで認証の仕組みだけです。実装は自分で行う必要があります。

また、Laravel UIという昔からあるbladeベースの認証基盤もありますが、BreezeではなくUIを今から使うメリットはあまり無いと思います。

ちなみに、UI/BreezeとFortifyを両方導入した場合、デフォルトだとFortifyの認証システムは機能しません(スキャフォールドに負ける)。よってFortifyのプロバイダなどで2要素認証の仕組みを組み込んでも、2要素認証を含めてFortify側の認証機能は働きません。

Fortifyを使う時はFortify単独で使用します。

LaravelFortifyをいつ使用するのが適切か疑問に思われるかもしれません。まず、Laravelのアプリケーションスターターキットで説明されているいずれかを使用している場合、Laravelのすべてのアプリケーションスターターキットはあらかじめ完全な認証実装を提供しているため、LaravelFortifyをインストールする必要はありません。 。

アプリケーションスターターキットを使用しておらず、アプリケーションに認証機能が必要な場合は、アプリケーションの認証機能を自分で実装するか、Laravel Fortifyを使用してこうした機能のバックエンド実装を提供するか、2つのオプションがあります。

Laravel 8.x Laravel Fortify」より引用

Fortifyの導入

まずはFortifyを導入します。次の流れを辿ってください。

Fortifyのインストール

composer require laravel/fortify
php artisan vendor:publish --provider="Laravel\Fortify\FortifyServiceProvider"
php artisan migrate

bootstrapのインストール

composer require laravel/ui
php artisan ui bootstrap
npm install && npm run dev

app.phpの編集

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

],

FortifyServiceProvider.phpの編集

public function boot()
{
    Fortify::loginView(fn () => view('auth.login'));
    Fortify::registerView(fn () => view('auth.register'));

    // ...
}

web.phpにルート追加

Route::get('/home', function () {
    return view('home');
})->middleware('auth');

app.blade.phpの作成

ここからコピペしてresources/views/layouts/app.blade.phpを作成。

login.blade.phpの作成

ここからコピペしてresources/views/auth/login.blade.phpを作成。

register.blade.phpの作成

ここからコピペしてresources/views/auth/register.blade.phpを作成。

home.blade.phpの作成

@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'))
                        <div class="alert alert-success" role="alert">
                            {{ session('status') }}
                        </div>
                    @endif

                    {{ __('You are logged in!') }}
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

動作確認

ここまででログイン周りの実装は完了です。実際に登録やログインが可能かどうかを確認してみてください。

2要素認証の実装

ここからが2要素認証の実装になります。2要素認証の実装については以下の動画を参考にしています。

User.phpにトレイトを追加

use Laravel\Fortify\TwoFactorAuthenticatable;

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

    // ...
}

FortifyServiceProvider.phpの編集

public function boot()
{
    Fortify::confirmPasswordView(function () {
        return view('auth.confirm-password');
    });
    Fortify::twoFactorChallengeView(function () {
        return view('auth.two-factor-challenge');
    });

    // ...
}

home.blade.phpを修正

@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-disabled")
                        <div class="alert alert-success" role="alert">
                            Two factor Authentication has been disabled.
                        </div>
                    @endif

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

                    <form method="POST" action ="/user/two-factor-authentication">
                        @csrf

                        @if (auth()->user()->two_factor_secret)
                            @method('DELETE')

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

                            <div>
                                <h3>Recovery Codes:</h3>
                                <ul>
                                    @foreach (json_decode(decrypt(auth()->user()->two_factor_recovery_codes)) as $code)
                                        <li>{{ $code }}</li>
                                    @endforeach
                                </ul>
                            </div>
                            
                            <button class="btn btn-danger">Disable</button>
                        @else
                            <button class="btn btn-primary">Enable</button>
                        @endif
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

confirm-password.blade.phpの作成

ここからコピペしてresources/views/auth/confirm-password.blade.phpを作成。

two-factor-challenge.blade.phpの作成

@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">
                    {{ __('Please enter your authentication code to login.') }}

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

                        <div class="form-group row">
                            <label for="code" class="col-md-4 col-form-label text-md-right">{{ __('Code') }}</label>

                            <div class="col-md-6">
                                <input id="code" type="code" class="form-control @error('code') is-invalid @enderror" name="code" required autocomplete="current-code">

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

                        <div class="form-group row mb-0">
                            <div class="col-md-8 offset-md-4">
                                <button type="submit" class="btn btn-primary">
                                    {{ __('Submit') }}
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>

            <div class="card">
                <div class="card-header">{{ __('Two factor Recovery Code') }}</div>

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

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

                        <div class="form-group row">
                            <label for="recovery_code" class="col-md-4 col-form-label text-md-right">{{ __('Reocvery Code') }}</label>

                            <div class="col-md-6">
                                <input id="recovery_code" type="recovery_code" class="form-control @error('recovery_code') is-invalid @enderror" name="recovery_code" required autocomplete="current-recovery_code">

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

                        <div class="form-group row mb-0">
                            <div class="col-md-8 offset-md-4">
                                <button type="submit" class="btn btn-primary">
                                    {{ __('Submit') }}
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>

        </div>
    </div>
</div>
@endsection

Google AuthenticatorでQRコードを登録

これでFortifyを使った2要素認証の実装は完了です。

QRコードをGoogle Authenticatorで読み取ってください。また、リカバリコードでもログインできることを確認してみてください。

QRコードの表示場所を変える

ホーム画面に2要素認証のQRコードが表示されるのは変ですので、プロフィール画面を作ってそこに表示するようにします。

app.blade.phpの修正

app.blade.phpのリスト要素を以下のように修正します。

<li class="nav-item dropdown">
    <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
        {{ Auth::user()->name }}
    </a>

    <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
        <a class="dropdown-item" href="{{ route('logout') }}"
        onclick="event.preventDefault();
                        document.getElementById('logout-form').submit();">
            {{ __('Logout') }}
        </a>

        <form id="logout-form" action="{{ route('logout') }}" method="POST" class="d-none">
            @csrf
        </form>

        <a class="dropdown-item" href="{{ route('profile') }}">
            {{ __('Profile') }}
        </a>
    </div>
</li>

web.phpの修正

ルートを以下のように書き換えます。

Route::middleware('auth')->group(function () {
    Route::get('/home', function () {
        return view('home');
    });
    Route::get('/profile', function() {
        return view('profile.index');
    })->name('profile');
});

profile/index.blade.phpの作成

home.blade.phpの内容をresources/views/profile/index.blade.phpにコピーしてください。home.blade.phpは@extends(‘layouts.app’)以外を削除します。

これでQRコードがhomeからいなくなります。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)