Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: Rodauthで多要素認証を実装する(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

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には、上で述べたさまざまな多要素認証方法のサポートが組み込まれています。

jeremyevans/rodauth - GitHub

他の多要素認証ライブラリ1と比べて、Rodauthは「完全なエンドポイント」「デフォルトのHTMLテンプレート」「セッション管理」「ロックアウトロジック」などをより進んだ形で統合した体験を提供しています2。本チュートリアルでは手を広げすぎないよう、最も一般的な3つの手法に絞って実装することにします。

ここではrodauth-rails gemを使い、前回の記事で構築し始めたアプリケーションを引き続き使うことにします。

janko/rodauth-rails - GitHub

本記事で目標とする機能は、ユーザーがメインの多要素認証方法としてTOTPを設定できるようにし、バックアップの多要素認証方法としてSMSコードとリカバリーコードを使えるようにすることです。

🔗 TOTP

TOTP機能は、Rodauthのワンタイムパスワード機能であるotpによって提供されます。これはrotp gemとrqrcode gemに依存しているので、まずそれらをインストールしましょう。

$ bundle add rotp rqrcode

mdp/rotp - GitHub
whomwah/rqrcode - GitHub

次に、必要なデータベーステーブルを作成する必要があります。作成には、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

Rodauth OTP setup page

これで、ユーザーはスマホの認証アプリ(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

Rodauth TOTP authentication page

🔗 リカバリーコード

ユーザーが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を設定すると次のようなページが表示されます。

Rodauth page for viewing and downloading recovery codes

ユーザーが次回アカウントにログインするときに、多要素認証ページでTOTPの代わりにリカバリーコードの入力を選択可能になります。

Multifactor auth page with OTP and recovery codes options

🔗 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/twilio-ruby - GitHub

次に、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認証のセットアップを完了できるようになります。

Rodauth SMS authentication setup page

以後、ユーザーがログインするときには、TOTPやリカバリーコードによる認証に加えて、SMS認証も選択できるようになります。

🔗 多要素認証を無効にする

Rodauthは、セットアップと認証用のエンドポイントに加えて、多要素認証の無効化用エンドポイントも提供します(無効化にはパスワードの確認を求められます)。

  • /otp-disable: ワンタイムパスワード認証を無効にする
  • /sms-disable: SMS認証を無効にする
  • /multifactor-disable: すべての多要素認証を無効にする

以前に設定した多要素認証の個別の認証要素を無効にするリンクは、多要素認証の管理ページで自動的に表示されます。

Rodauth links for disabling configured MFA methods

多要素認証の認証方法を無効にすると、アカウントに関連付けられているレコードは対応するデータベーステーブルから削除されます。

🔗 最後に

本チュートリアルでは、Rodauthとrodauth-railsを使ってRailsに多要素認証機能を追加する方法を紹介しました。ユーザーがTOTPを主要な多要素認証方法として設定できるようにし、その後でリカバリーコードのセットも受け取り、SMSもバックアップの多要素認証方法のひとつとして設定可能になります。

Rodauthは、さまざまな多要素認証を管理する完全なエンドポイントとデフォルトのHTMLテンプレートを備えていることを見てきました。Rodauthは、他の認証ライブラリよりも一体化された経験を提供しています。多要素認証機能が近年ますます一般的な要件となっていることを考慮すると、他の認証機能と同じサポートレベルで多要素認証もサポートするフレームワークは実に便利です。

関連記事

Rails: Rodauthでパスキー認証を行う(翻訳)

Rails: RodauthでSNSログインを行う(翻訳)

Rails: 認証gem 'rodauth-rails' README(翻訳)


  1. 原注: 本記事執筆時点で最も広く使われているライブラリは、devise-two-factoractive_model_otptwo_factor_authenticationです。 
  2. 原注: 詳しくは以下のソースコードを参照してください: OTPsms_codesrecovery_codestwo_factor_base 
  3. 原注: デフォルトのテンプレートは、rails generate rodauth:views otp and modifying app/views/rodauth/otp_setup.html.erbを実行することでオーバーライドできます。 

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。