Rails: Rodauthで多要素認証を実装する(翻訳)
多要素認証(MFA: Multi-factor authentication)は、一般化された2要素認証(2FA: two-factor authentication)のことです。この認証方法では、ユーザーがアクセスを許可されるために証拠(「要素」)を2つ以上提供する必要があります。
通常、ユーザーは最初に自分だけが知っている情報(例: パスワード)を示し、次に自分だけが所有しているもの(例: 別のデバイス)を示すことで認証を行います。これにより、ユーザーアカウントにセキュリティの層が追加されます。
多要素認証で最も一般的に見られるのは、以下のような手法です。
- TOTP (Time-based One-Time Passwords)
- ユーザーは、デバイスにインストールされたアプリを用いて認証コードを表示します。このコードは30秒ごとに更新されます。
- SMSコード
- アプリケーションから認証コードを要求されると、ユーザーは認証コードを携帯電話のSMSメッセージで受け取ります。
- リカバリーコード
- 1回だけ利用できる固定のコードセットをユーザーに提供し、これをログイン時に入力できます(リカバリーコードはバックアップの手段として使われるのが普通です)。
- WebAuthn
- ユーザーは、セキュリティキーや、プラットフォーム組み込みの生体認証センサ(指紋認証など)で自分自身を認証します。
本記事では、Rodauthを使ってRailsアプリに多要素認証を追加する方法を紹介します。Rodauthには、上で述べたさまざまな多要素認証方法のサポートが組み込まれています。
他の多要素認証ライブラリ1と比べて、Rodauthは「完全なエンドポイント」「デフォルトのHTMLテンプレート」「セッション管理」「ロックアウトロジック」などをより進んだ形で統合した体験を提供しています2。本チュートリアルでは手を広げすぎないよう、最も一般的な3つの手法に絞って実装することにします。
ここではrodauth-rails gemを使い、前回の記事で構築し始めたアプリケーションを引き続き使うことにします。
本記事で目標とする機能は、ユーザーがメインの多要素認証方法としてTOTPを設定できるようにし、バックアップの多要素認証方法としてSMSコードとリカバリーコードを使えるようにすることです。
🔗 TOTP
TOTP機能は、Rodauthのワンタイムパスワード機能であるotp
によって提供されます。これはrotp gemとrqrcode gemに依存しているので、まずそれらをインストールしましょう。
$ bundle add rotp rqrcode
次に、必要なデータベーステーブルを作成する必要があります。作成には、rodauth-railsが提供するマイグレーションジェネレーターを使います。
$ rails generate rodauth:migration otp
# create db/migrate/20201214200106_create_rodauth_otp.rb
$ rails db:migrate
# == 20201214200106 CreateRodauthOtp: migrating =======================
# -- create_table(:account_otp_keys)
# == 20201214200106 CreateRodauthOtp: migrated ========================
これで、Rodauthのコンフィグでotp
機能を有効にできるようになります。
# app/misc/rodauth_main.rb
class RodauthMain < Rodauth::Rails::Auth
configure do
# ...
enable :otp
end
end
これによって、アプリケーションに以下のルーティングが追加されます。
/otp-auth
: TOTPコードで認証する/otp-setup
: TOTP認証をセットアップする/otp-disable
: TOTP認証を無効にする/multifactor-manage
: 現在利用可能な多要素認証の手法をセットアップ/管理する/multifactor-auth
: 利用可能な多要素認証の手法で認証する/multifactor-disable
: すべての多要素認証を無効にする
ユーザーが多要素認証を設定できるように、多要素認証の方法(要素)を管理する/multifactor-manage
ルーティングへのリンクをビューに表示しましょう。
<!-- app/views/application/_navbar.html.erb -->
<% if rodauth.logged_in? %>
<!-- ... --->
<%= link_to "Manage MFA", rodauth.two_factor_manage_path, class: "dropdown-item" %>
<!-- ... --->
<% end %>
ユーザーがログインして、左上ドロップダウンボックスの「Manage MFA」をクリックすると、ワンタイムパスワードのセットアップページにリダイレクトされます。このページは、Rodauthがデフォルトで提供しています3。
これで、ユーザーはスマホの認証アプリ(Google Authenticator、Microsoft Authenticator、Authyなど)を使ってQRコードをスキャンし、ワンタイムパスワードのコードを(現在のパスワードと一緒に)Railsアプリに入力することで、ワンタイムパスワードの設定を完了できます。
開発者は、ROTP gemで以下のようにワンタイムパスワードのsecretからコードを生成できます。
$ rotp --secret omo2p3movepqyc222rp54v3cic7ky2au
409761
次は、ワンタイムパスワードが設定済みのユーザーがログインしたら、自動的にワンタイムパスワード認証ページにリダイレクトされるようにしたいと思います。
これを実現するためには、多要素認証を設定したログイン済みのユーザーに対して2要素認証を行うよう要求し、flashメッセージを微調整してサインイン操作と一体化させます。
# app/misc/rodauth_app.rb
class RodauthApp < Rodauth::Rails::App
# ...
route do |r|
# ...
# ユーザーがログイン済みで多要素認証セットアップが完了している場合は
# 多要素認証を必須にする
if rodauth.uses_two_factor_authentication?
rodauth.require_two_factor_authenticated
end
end
end
# app/misc/rodauth_main.rb
class RodauthMain < Rodauth::Rails::Auth
configure do
# ...
# ログイン後のリダイレクトではエラーメッセージを表示しない
two_factor_need_authentication_error_flash { flash[:notice] == login_notice_flash ? nil : super() }
# 一般的な認証メッセージを表示する
two_factor_auth_notice_flash { login_notice_flash }
end
end
🔗 リカバリーコード
ユーザーがTOTPを設定した後にユーザーが保存しておける「回復用」のリカバリーコードのセットも生成することをおすすめします。これは、TOTPデバイスにアクセスできなくなった場合のログインに利用できます。この機能はRodauthのrecovery_codes
機能で提供されています。
最初に、必要なデータベーステーブルを作成しましょう。
$ rails generate rodauth:migration recovery_codes
# create db/migrate/20201214200106_create_rodauth_recovery_codes.rb
$ rails db:migrate
# == 20201217071036 CreateRodauthRecoveryCodes: migrating =======================
# -- create_table(:account_recovery_codes, {:primary_key=>[:id, :code]})
# == 20201217071036 CreateRodauthRecoveryCodes: migrated ========================
次に、Rodauthのコンフィグでrecovery_codes
機能を有効にします。
# app/misc/rodauth_main.rb
class RodauthMain < Rodauth::Rails::Auth
configure do
# ...
enable :otp, :recovery_codes
end
end
これにより、アプリに以下のルーティングが追加されます。
/recovery-auth
: リカバリーコードで認証する/recovery-codes
: リカバリーコードの表示・追加
デフォルトのリダイレクトではなく、ユーザーがTOTPを正常に設定完了した後にリカバリーコードを表示するために、after_otp_setup
フックをオーバーライドします。
# app/misc/rodauth_main.rb
class RodauthMain < Rodauth::Rails::Auth
configure do
# ...
# 最初の多要素認証方法が有効になったら自動的にリカバリーコードを生成する
auto_add_recovery_codes? true
# 最後の多要素認証方法が無効になったら自動的にリカバリーコードを削除する
auto_remove_recovery_codes? true
# TOTPセットアップ後にリカバリーコードを表示する
after_otp_setup do
set_notice_now_flash "#{otp_setup_notice_flash}, please make note of your recovery codes"
return_response add_recovery_codes_view
end
end
end
デフォルトのRodauthビューテンプレートをオーバーライドして、リカバリーコードをより見やすい方法で表示することにします。利便性のため、リカバリーコードのダウンロードリンクも追加します。セキュリティを維持するためのパスワード保護が必要なエンドポイントを新たに追加する代わりに、シンプルなHTML内でデータURLとdownload
属性を利用してダウンロードリンクを実装します。
$ rails generate rodauth:views recovery_codes
<!-- app/views/rodauth/add_recovery_codes.html.erb -->
<% content_for :title, rodauth.add_recovery_codes_page_title %>
<% if rodauth.recovery_codes.any? %>
<p class="my-3">
Copy these recovery codes to a safe location.
You can also download them <%= link_to "here", "data:,#{rodauth.recovery_codes.join("\n")}", download: "myapp-recovery-codes.txt" %>.
</p>
<div class="d-inline-block mb-3 border border-info rounded px-3 py-2">
<% rodauth.recovery_codes.each_slice(2) do |code1, code2| %>
<div class="row text-info text-left">
<div class="col-lg my-1 font-monospace"><%= code1 %></div>
<div class="col-lg my-1 font-monospace"><%= code2 %></div>
</div>
<% end %>
</div>
<% end %>
<!-- Used for filling in missing recovery codes later on -->
<% if rodauth.can_add_recovery_codes? %>
<%== rodauth.add_recovery_codes_heading %>
<%= render template: "rodauth/recovery_codes", layout: false %>
<% end %>
これで、ユーザーがTOTPを設定すると次のようなページが表示されます。
ユーザーが次回アカウントにログインするときに、多要素認証ページでTOTPの代わりにリカバリーコードの入力を選択可能になります。
🔗 SMSコード
2要素認証で、TOTP機能の他にSMSコード機能も提供しておくのは良い習慣です。Rodauthは、専用のsms_codes
機能を提供しています。
今度も、SMSコードをセットアップするのに必要なデータベーステーブルを作成します。
$ rails generate rodauth:migration sms_codes
# create db/migrate/20201219173710_create_rodauth_sms_codes.rb
$ rails db:migrate
# == 20201219173710 CreateRodauthSmsCodes: migrating ==================
# -- create_table(:account_sms_codes)
# == 20201219173710 CreateRodauthSmsCodes: migrated ===================
続いて、Rodauthコンフィグでsms_codes
機能を有効にします。
# app/misc/rodauth_main.rb
class RodauthMain < Rodauth::Rails::Auth
configure do
# ...
enable :otp, :recovery_codes, :sms_codes
end
end
これにより、アプリに以下のルーティングが追加されます。
/sms-request
: 送信されるSMSコードをリクエストする/sms-auth
: SMSコードで認証する/sms-setup
: SMSコード認証をセットアップする/sms-confirm
: 提供された電話番号を確認する/sms-disable
:SMSコード認証を無効にする
ユーザーからSMSコードの送信をリクエストされると、Rodauthは設定済みの電話番号と対応するテキストメッセージを使ってsms_send
メソッドを呼び出します。
なお、このメソッドはデフォルトでは定義されていません(SMSをどのように送信したいかはRodauthにはわからないからです)。代わりに、開発者はsms_send
を明示的に実装することが期待されています。
# app/misc/rodauth_main.rb
class RodauthMain < Rodauth::Rails::Auth
configure do
# ...
sms_send do |phone, message|
# ここを実装する必要あり
end
end
end
ここではTwilioでSMSメッセージを送信することにします。Twilioアカウントが設定済みという前提で、TwilioのアカウントSID、認証トークン、電話番号をRailsの認証情報に追加します。
$ rails credentials:edit
twilio:
account_sid: <YOUR_ACCOUNT_SID>
auth_token: <YOUR_AUTH_TOKEN>
phone_number: <YOUR_PHONE_NUMBER>
次に、twilio-ruby gemをインストールし、設定された認証情報を利用するTwilioクライアントのラッパークラスを作成します。
$ bundle add twilio-ruby
# app/misc/twilio_client.rb
class TwilioClient
Error = Class.new(StandardError)
InvalidPhoneNumber = Class.new(Error)
def initialize
@account_sid = Rails.application.credentials.twilio.account_sid!
@auth_token = Rails.application.credentials.twilio.auth_token!
@phone_number = Rails.application.credentials.twilio.phone_number!
end
def send_sms(to, message)
client.messages.create(from: @phone_number, to: to, body: message)
rescue Twilio::REST::RestError => error
# 詳しくは以下を参照: https://www.twilio.com/docs/api/errors/21211
raise TwilioClient::InvalidPhoneNumber, error.message if error.code == 21211
raise TwilioClient::Error, error.message
end
def client
Twilio::REST::Client.new(@account_sid, @auth_token)
end
end
最後に、sms_send
を新しいTwilioClient
クラスで実装します。SMSの送信エラーをバリデーションエラーに変換し、電話番号とコードが永続化されないように外側のデータベーストランザクションをロールバックします。
# app/misc/rodauth_main.rb
class RodauthMain < Rodauth::Rails::Auth
configure do
# ...
sms_send do |phone, message|
twilio = TwilioClient.new
twilio.send_sms(phone, message)
rescue TwilioClient::Error => error
db.rollback_on_exit
throw_error_status(422, sms_phone_param, sms_invalid_phone_message) if error.is_a?(TwilioClient::InvalidPhoneNumber)
throw_error_status(500, sms_phone_param, "sending the SMS code failed")
end
end
end
これで、ユーザーが多要素認証の管理ページでSMS認証設定ページを開き、電話番号とパスワードを入力すると、ユーザーが受け取ったSMSコードを入力してSMS認証のセットアップを完了できるようになります。
以後、ユーザーがログインするときには、TOTPやリカバリーコードによる認証に加えて、SMS認証も選択できるようになります。
🔗 多要素認証を無効にする
Rodauthは、セットアップと認証用のエンドポイントに加えて、多要素認証の無効化用エンドポイントも提供します(無効化にはパスワードの確認を求められます)。
/otp-disable
: ワンタイムパスワード認証を無効にする/sms-disable
: SMS認証を無効にする/multifactor-disable
: すべての多要素認証を無効にする
以前に設定した多要素認証の個別の認証要素を無効にするリンクは、多要素認証の管理ページで自動的に表示されます。
多要素認証の認証方法を無効にすると、アカウントに関連付けられているレコードは対応するデータベーステーブルから削除されます。
🔗 最後に
本チュートリアルでは、Rodauthとrodauth-railsを使ってRailsに多要素認証機能を追加する方法を紹介しました。ユーザーがTOTPを主要な多要素認証方法として設定できるようにし、その後でリカバリーコードのセットも受け取り、SMSもバックアップの多要素認証方法のひとつとして設定可能になります。
Rodauthは、さまざまな多要素認証を管理する完全なエンドポイントとデフォルトのHTMLテンプレートを備えていることを見てきました。Rodauthは、他の認証ライブラリよりも一体化された経験を提供しています。多要素認証機能が近年ますます一般的な要件となっていることを考慮すると、他の認証機能と同じサポートレベルで多要素認証もサポートするフレームワークは実に便利です。
関連記事
- 原注: 本記事執筆時点で最も広く使われているライブラリは、devise-two-factor、active_model_otp、two_factor_authenticationです。 ↩
- 原注: 詳しくは以下のソースコードを参照してください: OTP、sms_codes、recovery_codes、two_factor_base ↩
-
原注: デフォルトのテンプレートは、
rails generate rodauth:views otp and modifying app/views/rodauth/otp_setup.html.erb
を実行することでオーバーライドできます。 ↩
概要
原著者の許諾を得て翻訳・公開いたします。