RailsとLaravelで比較する2FA導入方法|TOTPとFIDOキー(WebAuthn)で学ぶ多要素認証実装ガイド

RailsからLaravelを眺める

※本記事は「RailsからLaravelを眺める」シリーズの第9回です。
Rails出身の私がLaravelを触りながら、Railsと比較して違いを整理していく連載になります。


はじめに

Webアプリケーションのセキュリティを考えるとき、パスワードだけに頼るのは心許ないというのが現実です。パスワードの漏洩や総当たり攻撃、フィッシングなど、ユーザー認証が破られるリスクは常に存在しています。その対策として有効なのが、多要素認証(2FA: Two Factor Authentication)です。

2FAの中でも代表的なのは、Google Authenticatorなどで使われるTOTP(Time-based One Time Password)と、YubiKeyなどの物理デバイスを利用するFIDO2(WebAuthn)方式です。TOTPは30秒ごとに更新される6桁のコードを入力させる仕組みで、比較的導入が容易です。一方、FIDO2は公開鍵暗号方式をベースにしており、フィッシングに強くユーザー体験も良好ですが、実装の敷居がやや高めです。

今回の記事では、この多要素認証の代表格であるTOTPと、近年注目を集めるFIDOキーによる認証について取り上げ、Rails(rotp / webauthn-ruby)とLaravel(Google2FA / laravel-webauthn)でどのように導入できるのかを実際のコードとともに比較していきます。


Railsでの実装

まずはRailsからです。私はDevise用の devise-two-factor を使わず、シンプルに rotprqrcode を利用して自作するスタイルを取りました。その方が依存関係を減らせることと、フローを自分でコントロールしやすいからです。

Gemfileに以下を追加します。

gem 'rotp'
gem 'rqrcode'

UserモデルにはOTP用のシークレットを持たせ、新規作成時に自動で生成するようにします。

class User < ApplicationRecord
  before_create :set_otp_secret

  def set_otp_secret
    self.otp_secret = ROTP::Base32.random_base32
  end

  def totp
    ROTP::TOTP.new(otp_secret, issuer: "MyApp")
  end

  def verify_otp(code)
    totp.verify(code, drift_behind: 15)
  end
end

QRコードの生成は以下のように簡単です。

qr = RQRCode::QRCode.new(user.totp.provisioning_uri(user.email))

ビューでこのQRコードを表示すれば、ユーザーはGoogle Authenticatorに登録でき、以降はアプリが生成するワンタイムコードを使って認証が行えます。

認証時のコントローラは以下のようになります。

class TwoFactorAuthController < ApplicationController
  def verify
    if current_user.verify_otp(params[:otp_code])
      session[:otp_passed] = true
      redirect_to dashboard_path
    else
      flash[:alert] = "ワンタイムパスコードが正しくありません"
      render :new
    end
  end
end

時刻のズレを考慮するために drift_behind を指定し、数十秒の誤差を許容するようにしています。また、端末を紛失した場合のためにリカバリーコードを発行しておくとユーザー体験が向上します。

続いてFIDO2、つまりWebAuthnによる認証です。Railsでは webauthn-ruby が使われることが多いです。Gemfileに追加します。

gem 'webauthn'

フロントエンドで navigator.credentials.create を呼び出し、得られた公開鍵クレデンシャルをサーバー側に送ります。サーバー側で受け取ったクレデンシャルを保存する処理は次のように書けます。

class WebauthnRegistrationsController < ApplicationController
  def create
    credential = WebAuthn::Credential.from_create(params[:credential])
    credential.verify(challenge)

    current_user.credentials.create!(
      external_id: credential.id,
      public_key: credential.public_key,
      sign_count: credential.sign_count
    )

    head :ok
  end
end

認証時には navigator.credentials.get を利用し、受け取った署名をサーバー側で検証します。Deviseと組み合わせる場合は、Warden戦略を追加してログインフローに統合する形になります。


Laravelでの実装

次にLaravelです。TOTPは pragmarx/google2fa-laravel が定番で、FortifyやJetstreamと組み合わせるとスムーズに導入できます。

composer require pragmarx/google2fa-laravel

コントローラは以下のように書けます。

use PragmaRX\Google2FA\Google2FA;

class TwoFactorController extends Controller
{
    public function enable(Request $request)
    {
        $google2fa = new Google2FA();
        $secret = $google2fa->generateSecretKey();

        $request->user()->update(['google2fa_secret' => $secret]);

        $qrUrl = $google2fa->getQRCodeUrl(
            'MyApp',
            $request->user()->email,
            $secret
        );

        return view('2fa.enable', ['qrUrl' => $qrUrl]);
    }

    public function verify(Request $request)
    {
        $google2fa = new Google2FA();

        if ($google2fa->verifyKey($request->user()->google2fa_secret, $request->input('otp'))) {
            session(['otp_passed' => true]);
            return redirect()->intended('dashboard');
        }

        return back()->withErrors(['otp' => 'コードが正しくありません']);
    }
}

ユーザーは表示されたQRコードをAuthenticatorアプリで登録し、以降は6桁のコードを入力するだけで認証が通ります。

FIDO2については laravel-webauthnlarapass が利用可能です。例えば larapass を使う場合、以下のように登録処理を書けます。

use DarkGhostHunter\Larapass\WebAuthn;

class WebauthnController extends Controller
{
    public function register(Request $request, WebAuthn $webauthn)
    {
        $credentials = $webauthn->validateAttestation($request);
        $request->user()->webauthnCredentials()->create($credentials);

        return response()->json(['status' => 'ok']);
    }

    public function login(Request $request, WebAuthn $webauthn)
    {
        $webauthn->validateAssertion($request, $request->user()->webauthnCredentials);

        return redirect()->intended('dashboard');
    }
}

フロントエンドはJavaScriptで navigator.credentials.createnavigator.credentials.get を呼び出し、その結果をサーバーに送ります。Laravelはミドルウェアを活用できるので、「このルートでは必ずFIDOキーによる認証が必要」といったルールを柔軟に設定できます。


まとめ

RailsとLaravelを比べると、Railsは rotp によるシンプルな自作スタイルが可能で、細かい制御を行いやすいのが特徴です。一方でLaravelは google2fa-laravellarapass といったパッケージが整備されており、導入が非常にスムーズです。

WebAuthnについては、Railsは webauthn-ruby で低レベルまで制御できるのに対し、Laravelはパッケージを使って素早く実装できる点に強みがあります。

どちらのフレームワークでもTOTPとFIDOキーの両方をサポートすることで、幅広いユーザーに対応できる堅牢な認証が実現できます。TOTPは導入のハードルが低く、ほとんどのユーザーが利用できます。FIDOキーはデバイスを持っているユーザーに限られますが、セキュリティとユーザー体験の両立という点で非常に有効です。

Rails出身の目線で見ると、Laravelは「パッケージを入れればすぐ使える」DXの良さが際立ちます。Railsはその代わりに、自分で組み立てられる自由度の高さが魅力です。どちらを選ぶかはプロジェクトの方針やチームの好みによりますが、いずれにしても2FAは「オプション」ではなく「標準装備」として考えるべき時代になったと感じます。

コメント

タイトルとURLをコピーしました