Railsアプリのログイン時に二段階認証を導入したかったのですが、一般的にどういった方法が用いられているのか分からなかったので調査しました。

開発中のRailsアプリに導入が何とか成功したので内容をしっかりと整理しておきます。

今回利用するgemでは、Railsアプリにdeviseが既に導入が完了している事を前提としますのでご了承下さい。

gemのインストール

Railsアプリに二段階認証を導入する場合、gemが良く利用されているようなので今回はgemを利用したいと思います。

二段階認証用のgemは幾つか存在しますが、今回は「tinfoil/devise-two-factor」というgemを利用します。

さらに、QRコードを生成して画面に表示するため「rqrcode」というgemも一緒にインストールします。

認証コードを生成するための専用アプリからQRコードを読み取って利用したいためです。

Gemfileに以下のコードを追加し、bundlerを利用してインストールを行います。

gem 'devise-two-factor'
gem 'rqrcode'

初期化

二段階認証を利用するためには、まず初期化を行わなければいけません。

以下の書式に合わせてコマンドを実行します。

# rails g devise_two_factor [モデル名] [暗号鍵の環境変数名]
例)
$ rails g devise_two_factor user SECRET_ENCRYPTION_KEY

今回の場合、ユーザー関連の情報としてUserというモデルを利用しているので、モデル名に「user」と指定し、暗号鍵の環境変数名の部分には「SECRET_ENCRYPTION_KEY」と指定しています。

実行後、Userモデルに以下の定義が追加されているかと思います。

devise :two_factor_authenticatable,
       otp_secret_encryption_key: ENV['SECRET_ENCRYPTION_KEY']

二段階認証の機能を付与するということで、「two_factor_authenticatable」という定義が追加されています。

あとは、暗号鍵を指定するために「otp_secret_encryption_key」という定義も追加されます。

暗号鍵をモデルに直書きしてしまうとセキュリティ面に問題あるかと思いますので、一般的には環境変数を用いて設定をします。

引数として環境変数を設定しているので、別途環境変数を設定する必要があります。

暗号鍵の実データはランダムな値を設定すれば良いので、rakeコマンドで生成された乱数を設定しましょう。

# 乱数を生成します
$ bin/rake secret

あと、Userモデルに二段階認証機能を付与したので、usersテーブルに二段階認証に必要な以下のカラムが追加されます。

  • encrypted_otp_secret
  • encrypted_otp_secret_iv
  • encrypted_otp_secret_salt
  • consumed_timestep
  • otp_required_for_login

二段階認証の登録

Strong Parameter の設定

二段階認証の登録フォームに、認証コードを入力するフィールドがあることが想定されます。

なので、登録フォームから認証コードも一緒にパラメータとして送れるようにします。

before_action :configure_sign_in_params, only: :create

def configure_sign_in_params
  devise_parameter_sanitizer.permit(:sign_in, keys: %w[otp_attempt])
end

QRコードの表示

QRコード用のURLを生成し、それを基にQRコードを生成します。

今回は、ヘルパーメソッドを用意してテンプレートにて利用する形にしたいと思います。

def qrcode
  # URLの生成
  # issuerはアプリに表示される名前
  url = current_user.otp_provisioning_uri(current_user.email, issuer: 'Your App')

  # QRコードの生成
  RQRCode::QRCode.new(url).as_svg(options).html_safe
end

このヘルパーメソッドをテンプレートにて呼び出せば、QRコードが表示されるようになります。

シークレットキーの生成

二段階認証をアプリに読み込む方法は、QRコードをスキャンする方法以外にも、シークレットキーを入力して読み込む方法もあります。

なので、シークレットキーを画面に表示させるとより親切設計になるかと思います。

モデルのgenerate_otp_secretメソッドを実行すると、シークレットキーが生成されます。

生成されたシークレットキーを、モデルのotp_secretという属性に設定して保存します。

# 認証コードのシークレットキーを生成しDBに保存する
current_user.otp_secret = User.generate_otp_secret
current_user.save!

これで生成したシークレットキーが参照できるようになったので、あとはテンプレートにてシークレットキーを表示させるようにします。

二段階認証の有効化

二段階認証の登録が正しく行われた場合、成功したことを記憶させておく必要があります。

まず、認証コードが正しいかを判定するためにvalidate_and_consume_otp!メソッドを利用して分岐させます。

認証コードが正しければ、モデルのotp_required_for_login属性をtrueに設定して保存します。

# 認証コードが正しいか
if user.validate_and_consume_otp!(認証コード)
  # 二段階認証の有効化
  current_user.otp_required_for_login = true
  current_user.save!

二段階認証の登録完了時

今回は、二段階認証の登録が完了した後に完了ページを表示することを想定します。

完了ページには、バックアップコードを表示させたいと思います。

バックアップコードの生成

バックアップコードを利用したい場合は、Userモデルに以下の定義を追加させます。

devise :two_factor_backupable

そして、usersテーブルにバックアップコード用のカラムを追加したいので、以下のマイグレーションファイルを用意してマイグレーションを実行します。

class AddDeviseTwoFactorBackupableToUsers < ActiveRecord::Migration
  def change
    # Change type from :string to :text if using MySQL database
    add_column :users, :otp_backup_codes, :string, array: true
  end
end

これでバックアップコードを利用する準備ができました。

バックアップコードを生成する場合は、generate_otp_backup_codes!メソッドを呼び出して保存し、コードの配列を受け取ります。

codes = current_user.generate_otp_backup_codes!
current_user.save!

あとは、テンプレートにてバックアップコードを表示させる処理を追加しましょう。

バックアップに関する詳細な設定は、以下のように設定が可能です。

devise :two_factor_backupable,
       # コードの文字数(長さ)
       otp_backup_code_length: 32,
       # 生成するコードの数
       otp_number_of_backup_codes: 10

認証コードの入力を促す

最後に、二段階認証が有効な場合はログイン後に二段階認証の認証コードの入力を促すように修正します。

二段階認証のフォーム入力画面では、以下の2種類のフォームが必要になるかと思います。

  • 認証コード
  • バックアップコード

テンプレートに上記のコード入力用のフォームを用意しましょう。

二段階認証が有効かどうかは、Userモデルのotp_required_for_login属性で判断できるのでそれに応じて分岐させます。

入力された認証コードが正しいかどうかは、先ほどのvalidate_and_consume_otp!メソッドで判断できますが、入力されたバックアップコードが正しいかどうかを判断するために、以下のメソッドで処理を分岐させます。

user.invalidate_otp_backup_code!(バックアップコード)

上記のどちらかの入力が確認できれば、通常のログイン後の画面に遷移させます。

投稿者: TWEI

趣味はプログラミング。 以前は仕事でプログラミングをやっていました。現在はWebエンジニアを目指して勉強中。 勉強で得た知識などをブログで発信していく予定です。

コメントをどうぞ

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA