※本記事は「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
を使わず、シンプルに rotp
と rqrcode
を利用して自作するスタイルを取りました。その方が依存関係を減らせることと、フローを自分でコントロールしやすいからです。
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-webauthn
や larapass
が利用可能です。例えば 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.create
と navigator.credentials.get
を呼び出し、その結果をサーバーに送ります。Laravelはミドルウェアを活用できるので、「このルートでは必ずFIDOキーによる認証が必要」といったルールを柔軟に設定できます。
まとめ
RailsとLaravelを比べると、Railsは rotp
によるシンプルな自作スタイルが可能で、細かい制御を行いやすいのが特徴です。一方でLaravelは google2fa-laravel
や larapass
といったパッケージが整備されており、導入が非常にスムーズです。
WebAuthnについては、Railsは webauthn-ruby
で低レベルまで制御できるのに対し、Laravelはパッケージを使って素早く実装できる点に強みがあります。
どちらのフレームワークでもTOTPとFIDOキーの両方をサポートすることで、幅広いユーザーに対応できる堅牢な認証が実現できます。TOTPは導入のハードルが低く、ほとんどのユーザーが利用できます。FIDOキーはデバイスを持っているユーザーに限られますが、セキュリティとユーザー体験の両立という点で非常に有効です。
Rails出身の目線で見ると、Laravelは「パッケージを入れればすぐ使える」DXの良さが際立ちます。Railsはその代わりに、自分で組み立てられる自由度の高さが魅力です。どちらを選ぶかはプロジェクトの方針やチームの好みによりますが、いずれにしても2FAは「オプション」ではなく「標準装備」として考えるべき時代になったと感じます。
コメント