LaravelにGoogle2FAを利用して2要素認証を実装する

最近、Laravelを触っていて、2要素認証の機能を入れられないか調べてみたら、[Google Authenticator](https://itunes.apple.com/jp/app/google-authenticator/id388497605)を利用した方法があったので、試してみた。
ライブラリは、[google2fa](https://github.com/antonioribeiro/google2fa)を利用。
実際の案件で使っていないので、実用化するにはもっと色々考慮しないといけないと思うけど、お試しということで。

# 前準備

前準備として、今回はLaravelの5.7を使って[ここ](https://readouble.com/laravel/5.7/ja/authentication.html)の手順で、基本的な認証機能を構築。

```.env``` ファイルを環境に合わせて編集して、

```shell
php artisan make:auth
php artisan migrate
```

で認証機能を実装。

# google2faの導入

## google2faをcomposerでインストール

```
composer require pragmarx/google2fa
```

## QRコードのライブラリをcomposerでインストール

QRコードを導入したいので、QRコードのライブラリ[BaconQrCode](https://github.com/Bacon/BaconQrCode)もインストール

```
composer require bacon/bacon-qr-code
```

# secret keyの登録

## カラムの追加

マイグレーションファイルを作成して、usersテーブルにsecret key用のカラムを追加

```
php artisan make:migration add_column_g2fa_key_users_table --table=users
```

カラム名などはお好みで。```down()```は省略。

```
public function up()
{
    Schema::table('users', function (Blueprint $table) {
        $table->string('g2fa_key')->nullable(true);
    });
}
```

マイグレーション実行

```
php artisan migrate
```

## Secret Keyの登録

今回は、ユーザー登録時にSecret Keyを登録するようにする。

```app/User.php
class User extends Authenticatable
{
    protected $fillable = [
        'name', 'email', 'password', 'g2fa_key', // g2fa_keyを追加
    ];
}
```

```app/Http/Controllers/Auth/RegisterController.php
use PragmaRX\Google2FA\Google2FA; // 追加

class RegisterController extends Controller
{
    protected function create(array $data)
    {
        // SecretKeyを生成
        $g2fa = new Google2FA();
        $key = $g2fa->generateSecretKey();
        return User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'password' => Hash::make($data['password']),
            'g2fa_key' => $key, // カラムに登録
        ]);
    }
}
```

```http://{your domain}/register``` からユーザー登録して、g2fa_keyにキーが登録されていればOK。

## QRコードの作成

homeにGoogle Authenticatorで読み取る用のQRコードを表示する。

```app/Http/Controllers/HomeController.php

use Illuminate\Support\Facades\Auth; // 追加
use PragmaRX\Google2FA\Google2FA; // 追加

class HomeController extends Controller
{
    public function index()
    {
        $user = Auth::user();

        $g2fa = new Google2FA();

        // $g2fa->setAllowInsecureCallToGoogleApis(true);
        $qrUrl = $g2fa->getQRCodeGoogleUrl(
            config('app.name'),
            $user->email,
            $user->g2fa_key
        );
        return view('home', ['qr' => $qrUrl]);
    }
}
```

おそらくだけど、ローカル環境はSSL対応ではないので、```$g2fa->setAllowInsecureCallToGoogleApis(true);```を追加する必要がある。

```resources/views/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">Dashboard</div>

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

                    <img src="{{ $qr }}"> {{--　追加 --}}
                </div>
            </div>
        </div>
    </div>
</div>
@endsection
```

QRコードが表示されるので、Google AuthenticatorアプリでQRを読み込む。

# 認証処理

ログイン画面側を作っていく

## Viewの修正

ログインフォームに認証キーの追加。一部だけ抜粋。

```resources/views/auth/login.blade.php
<div class="form-group row">
    <label for="secret_key" class="col-md-4 col-form-label text-md-right">{{ __('認証キー') }}</label>

    <div class="col-md-6">
        <input id="secret_key" type="text" class="form-control{{ $errors->has('secret_key') ? ' is-invalid' : '' }}" name="secret_key" value="{{ old('secret_key') }}" required>

        @if ($errors->has('secret_key'))
            <span class="invalid-feedback" role="alert">
                <strong>{{ $errors->first('secret_key') }}</strong>
            </span>
        @endif
    </div>
</div>
```

## LoginControllerの修正

LoginControllerに2要素認証のログインロジックを追加。追加した部分だけ抜粋。

```app/Http/Controllers/Auth/LoginController.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use PragmaRX\Google2FA\Google2FA;

class LoginController extends Controller
{
    public function authenticate(Request $request)
    {

        $email = $request->input('email');
        $password = $request->input('password');
        $secretKey = $request->input('secret_key');

        $credentials = ['email' => $email, 'password' => $password];

        if (Auth::once($credentials)) {
            $user = Auth::user();
            if ($user->g2fa_key) {
                $g2fa = new Google2FA();
                if (!$g2fa->verifyKey($user->g2fa_key, $secretKey)) {
                    return redirect()->route('login');
                }
                Auth::attempt($credentials);
            }
            return redirect()->intended($this->redirectTo);
        }

        return redirect()->route('login');
    }
}
```

## routerにログイン処理を追加

追加したメソッドでログイン処理を行うように、routerに下記を追加。

```routes/web.php
Route::post('login', 'Auth\LoginController@authenticate');
```

あとは、ログイン画面でログイン情報とGoogle Authenticatorで発行された番号を入力してログインできればOK

***

今回は、2段階ではなく2要素認証という形で試してみた。
バリデーションエラーの時とか、2段階にしたい時とかもっと考慮すべきことはたくさんあると思うけど、参考までにということで。
Laravelが触り始めたばかりでよくわかってないので、ログイン処理もお作法的に良いかなどは疑問。
あとは、[Google2FA for Laravel](https://github.com/antonioribeiro/google2fa-laravel)というのもあるらしい。

今回試したソースは、[ここ](https://github.com/y-shishido/2fa-sample)にアップしてます。

***
参考サイト

- [Laravel 5で2段階認証（2要素認証）を実装する方法（Google Authenticator利用）](https://s8a.jp/laravel-google-authenticator)