【Rails】devise-two-factorを使った2段階認証の実装方法【初学者】

2021年12月13日 11:59

はじめに

Railsアプリで2段階認証を実装するには、「rotp」というGemを使う方法の他に、「devise-two-factor」というGemを使う方法があります。「devise-two-factor」はその名の通り、IDとパスワードによる認証を実装する「devise」というGemの拡張機能です。

本記事では、IDとパスワードによる認証に加えて、「devise-two-factor」というGemを使った2段階認証を実装する方法について説明します。

なお、「rotp」を使った2段階認証の実装方法については以下の記事を参照してください。

実装の前提条件

認証機能の実装

認証機能は実装済みであるとします。IDとパスワードによる認証の実装については以下の記事を参照してください。

「devise」を使った認証機能の実装については以下の記事を参照してください。

なお、本記事では「devise」を使った認証が実装済みの前提で説明しています。

トークンソフトウェアの準備

ワンタイムパスワードの発行は電子メールやSMSで送信する方法の他に、トークンソフトウェアを使用する方法があります。今回、トークンソフトウェアの発行するワンタイムパスワードを使用する2段階認証を実装するので、「Google Authenticator」などのトークンソフトウェアを開発環境にインストールしておいてください。

2段階認証の実装

インストール

今回は2段階認証を実装する方法として「devise-two-factor」というGemを使用します。また、QRコードの作成には「rqrcode」というGemを使用します。

Gemfileに以下を追記し、bundle installを行います。

Gemfile

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

「devise-two-factor」の設定

モデルの設定

「devise-two-factor」の設定を行います。ターミナルで以下のコマンドを実行します。

$ rails generate devise_two_factor User OTP_SECRET_KEY

Userは認証モデル、OTP_SECRET_KEYは暗号化キーを保持する環境変数です。ご自分の環境に合わせて設定してください。

上記のコマンドを実行すると、Userモデルに「devise-two-factor」の設定が追記されます。

user.rb

class User < ApplicationRecord
  # 以下が追記される
  devise :two_factor_authenticatable,
         :otp_secret_encryption_key => ENV['OTP_SECRET_KEY']
end

暗号化キーの設定

「モデルの設定」セクションで設定した環境変数に暗号化キーを設定します。暗号化キーはotp_secretという「devise-two-factor」用のカラムの設定値として使用します。このカラムは「Encryptor」というGemで暗号化して保存されます。「Encryptor」は、バージョン3.0.0からデフォルトで長さが32バイト未満のキーを設定することができなくなりました。

そのため、今回使用する暗号化キーも32バイト以上のものを用意する必要があります。32バイト以上であれば何でもいいのですが、以下の方法でランダムな暗号化キーを生成することもできます。

$ rails console

> User.generate_otp_secret(32)
=> "2Q4BTKHOK4XHMTEPP5LMTHTFUPTKCCGK"

生成した暗号化キーを環境変数に設定します。

# 設定を追記
$ echo 'export OTP_SECRET_KEY="2Q4BTKHOK4XHMTEPP5LMTHTFUPTKCCGK"' >> ~/.bash_profile

# 設定を反映
$ source ~/.bash_profile

2段階認証の有効化の実装

ユーザーが2段階認証を有効化する画面および機能を作成します。

2段階認証の有効化:準備

以下のコマンドを実行して、2段階認証の有効化のコントローラーとビューを作成します。

$ rails generate controller two_step_verifications new create

2段階認証の有効化:有効化画面の実装

作成したコントローラーに以下を追記します。

two_step_verifications_controller.rb

class TwoStepVerificationsController < ApplicationController
  # 以下を追記
  before_action :authenticate_user!

  def new
    # 以下を追記
    unless current_user.otp_secret
      current_user.update!(otp_secret: User.generate_otp_secret(32))
    end

    issuer = 'YourAppName'
    label = "#{issuer}:#{current_user.email}"

    uri = current_user.otp_provisioning_uri(label, issuer: issuer)

    @qr_code = RQRCode::QRCode.new(uri)
      .as_png(resize_exactly_to: 200)
      .to_data_url
  end

  def create
  end
end

アプリ名とユーザーのメールアドレスを使用してissuerおよびlabelを作成し、その情報を元にURIを作成します。作成したURIを元にQRコードを作成します。

次に、作成したビューに以下を記述します。

new.html.erb

<h2>2-Step Verification</h2>

<%= form_tag otp_secrets_path, method: :post do %>
  <div class='field'>
    <%= image_tag @qr_code %>
  </div>

  <div class='field'>
    <%= label_tag :otp_attempt, 'Verify' %> <i>(enter a one-time password)</i><br />
    <%= text_field_tag :otp_attempt %>
  </div>

  <div class='actions'>
    <%= submit_tag 'Verify' %>
  </div>
<% end %>

ビューでは、コントローラーで作成したQRコード、トークンソフトウェアで発行されたワンタイムパスワードを入力するテキストボックス、送信ボタンを実装します。

最後に、routes.rbに以下を追記します。

routes.rb

Rails.application.routes.draw do
  # 以下を追記
  resources :two_step_verifications, only: [:new]
end

2段階認証の有効化:有効化画面の動作確認

ここまでの実装を確認します。テストサーバーを起動し、http://localhost:3000/two_step_verification/newにアクセスします。以下のような画面が表示されればOKです。

画面に表示されているQRコードをトークンソフトウェアで読み取るとアカウントが追加されます。以下は「Google Authenticator」でアカウントを追加した場合の例です。

2段階認証の有効化:有効化機能の実装

作成したコントローラーに以下を追記します。

two_step_verifications_controller.rb

class OtpSecretsController < ApplicationController
  ...

  def create
    # 以下を追記
    if current_user.validate_and_consume_otp!(params[:otp_attempt])
      current_user.update!(otp_required_for_login: true)
      redirect_to root_path
    else
      issuer = 'YourAppName'
      label = "#{issuer}:#{current_user.email}"

      uri = current_user.otp_provisioning_uri(label, issuer: issuer)

      @qr_code = RQRCode::QRCode.new(uri)
        .as_png(resize_exactly_to: 200)
        .to_data_url

      render :new
    end
  end
end

validate_and_consume_otpメソッドは、引数に渡されたワンタイムパスワード(フォームから送信されたワンタイムパスワード)を元に認証を行います。

認証結果がOKの場合、ユーザー情報を更新し任意のページにリダイレクトします。認証結果がNGの場合、QRコードを再作成し、2段階認証の有効化画面を再レンダリングします。

最後に、routes.rbに以下を追記します。

routes.rb

Rails.application.routes.draw do
  # 以下を修正
  resources :two_step_verification, only: [:new, :create]
end

2段階認証の有効化:有効化機能の動作確認

ここまでの実装を確認します。テストサーバーを起動し、http://localhost:3000/two_step_verification/newにアクセスします。

トークンソフトウェアに表示されているワンタイムパスワードをテキストボックスに入力し、2段階認証の有効化が成功することを確認します。また、適切でないワンタイムパスワードを入力し、2段階認証の有効化が失敗することも確認します。なお、「2段階認証の有効化:有効化画面の実装」セクションで確認した画面を更新した場合、トークンソフトウェアのアカウントを削除してアカウント追加からやり直してください。

データベースがちゃんと設定されているかを確認します。コンソールで以下のコマンドを実行します。

$ rails console

> user = User.find_by(email: "test@example.com")
> user.encrypted_otp_secret
=> "jxISK03iHpvigKUXAZ6Mm3TL146hC0HzJNFB4aZoWIJiMtd0s0jt+8v2hteC\nrolE\n"
> user.otp_required_for_login
=> true

2段階認証の実装

IDとパスワードによる認証の後にワンタイムパスワードによる認証が行われるよう実装します。

2段階認証:認証画面の実装

まず、IDとパスワードによる認証の処理を変更します。IDとパスワードによる認証の実装に「devise」を使っている場合、コントローラーの処理はすべてブラックボックス化されているため、このままでは2段階認証を追加することができません。そこで、「devise」のカスタムコントローラーを作成し、コントローラーの処理をカスタマイズします。

以下のコマンドを実行して、「devise」のカスタムコントローラーを作成します。

$ rails generate devise:controllers users

上記のコマンドを実行すると、app/controllers/users/ディレクトリ配下にサインアップやパスワード変更など、すべてのカスタムコントローラーが作成されます。カスタマイズはコントローラーのアクション毎に行うことができ、カスタムアクションを実装しなければ従来通りの処理が行われます。今回カスタマイズする必要があるのはログイン処理(sessions#create)だけです。

ログイン処理(sessions#create)のカスタムアクションを実装します。sessions_controller.rbに以下を追記します。

sessions_controller.rb

class Users::SessionsController < Devise::SessionsController
  # POST /resource/sign_in
  # 以下を追記
  def create
    @user = User.find_by(email: params[:user][:email])
    if @user.valid_password?(params[:user][:password])
      session[:otp_user_id] = @user.id
      redirect_to users_two_step_verification_path and return
    else
      flash.now[:alert] = 'Invalid Email or password.'
      render :new
    end
  end

  def new_two_step_verification
  end
end

ログイン処理(sessions#create)では、ユーザーが入力したIDおよびパスワードの妥当性のみ確認します(この時点ではログイン処理を行わない)。妥当であればIDをセッションに保存してから2段階認証の認証画面にリダイレクトし、妥当でなければログイン画面を再レンダリングします。

リダイレクト先のアクション(sessions#new_two_step_verification)では何も行いません(暗黙的なレンダリングにより、アクション名と同名のビューがレンダリングされる)。

最後に、作成したカスタムコントローラーのルーティングを設定します。routes.rbに以下を追記します。

routes.rb

Rails.application.routes.draw do
  # 以下を追記
  devise_for :users, controllers: {
    sessions: 'users/sessions'
  }
  devise_scope :user do
    patch '/users/sign_in', to: 'users/sessions#create'
  end
end

「devise」のデフォルトのスコープはdeviseのため、devise_forの第1引数にusersを指定しスコープを変更しています。

次に、2段階認証を行う画面を実装します。この画面のビューは「devise」のログイン処理に追加したいので、「devise」のカスタムビューを作成します。作成したカスタムビュー自体の変更は行いませんが、2段階認証の認証画面のビューをその中に作成します。

以下のコマンドを実行して、「devise」のカスタムビューを作成します。

$ rails generate devise:views users

上記のコマンドを実行すると、app/views/users/ディレクトリ配下にサインアップやパスワード変更など、すべてのカスタムビューが作成されます。この中のsessions/ディレクトリ配下にnew_two_step_verification.html.erbファイルを作成します。

作成したビューに以下を記述します。

new_two_step_verification.html.erb

<h2>2-Step Verification</h2>

<%= form_tag users_two_step_verification_path, method: :post do %>
  <div class='field'>
    <%= label_tag :otp_attempt, 'Verify' %> <i>(enter a one-time password)</i><br />
    <%= text_field_tag :otp_attempt %>
  </div>

  <div class='actions'>
    <%= submit_tag 'Verify' %>
  </div>
<% end %>

トークンソフトウェアに表示されたワンタイムパスワードを入力するテキストボックスと送信ボタンのみのシンプルなフォームです。

最後に、2段階認証の認証画面のルーティングを追加します。routes.rbに以下を追記します。

routes.rb

Rails.application.routes.draw do
  devise_scope :user do
    patch '/users/sign_in', to: 'users/sessions#create'
    # 以下を追記
    get '/users/two_step_verification', to: 'users/sessions#new_two_step_verification'
  end
end

2段階認証:認証画面の動作確認

ここまでの実装を確認します。テストサーバーを起動し、http://localhost:3000/users/sign_inにアクセスします。IDとパスワードを入力し、ログインボタンをクリックすると、以下のような2段階認証の認証画面が表示されることを確認します。

2段階認証:認証機能の実装

「devise」のカスタムコントローラーに2段階認証の認証機能を実装します。sessions_controller.rbに以下を追記します。

sessions_controller.rb

class Users::SessionsController < Devise::SessionsController
  ...

  # 以下を追記
  def create_two_step_verification
    user = User.find(session[:otp_user_id])

    if user.validate_and_consume_otp!(params[:otp_attempt])
      sign_in(user)
      redirect_to root_path
    else
      render :users_new_two_step_verification
    end
  end
end

認証結果がOKの場合、「devise」のログイン処理を行い、任意のページにリダイレクトします。認証結果がNGの場合、2段階認証の認証画面を再レンダリングします。

次に、routes.rbに以下を追記します。

routes.rb

Rails.application.routes.draw do
  devise_scope :user do
    patch '/users/sign_in', to: 'users/sessions#create'
    get '/users/two_step_verification', to: 'users/sessions#new_two_step_verification'
    # 以下を追記
    post '/users/two_step_verification', to: 'users/sessions#create_two_step_verification'
  end
end

2段階認証:認証機能の動作確認

ここまでの実装を確認します。テストサーバーを起動し、http://localhost:3000/users/sign_inにアクセスします。IDとパスワードを入力し、ログインボタンをクリックします。

表示された2段階認証の認証画面でワンタイムパスワードを入力し、送信ボタンをクリックすると、ログインが成功することを確認します。また、適切でないワンタイムパスワードを入力してログインが失敗することも確認します。

以上で「devise-two-factor」を使った2段階認証の実装は完了です。

まとめ

「devise-two-factor」には、ユーザーがトークンソフトウェアの設定を消してしまうなどしてワンタイムパスワードが入力できなくなってしまったときに復旧するための手段として、リカバリコードを生成する機能なども実装することができます。また、2段階認証はユーザーがいつでも有効化/無効化を切り替えられるようにしたほうがいいでしょう。

本記事を参考にして、「devise-two-factor」を使った2段階認証を実装していただければと思います。

関連記事

【Rails】M1チップ搭載MacでRuby on Railsの開発環境構築
# はじめに M1チップ搭載MacにRuby on Railsの開発環境を構築する手順を記載します。 - MacBook Air (M1, 2020) - macOS Monterey 12.3.1 # Homebrew ## [...]
2022年5月5日 11:56
【Rails】Rakeタスクの基本情報と作成・実行方法
# はじめに Railsには標準でRakeというGemが同梱されています。RakeはRubyで実装されたMake(UNIX系のOSで使用できるコマンド)のようなビルド作業を自動化するツールです。Ruby Make、略してRakeというわけですね。 [...]
2022年3月7日 22:12
【Rails】モデルに外部キーを設定する方法とよく起こるエラー内容について
# はじめに Railsでモデルに外部キーを設定する方法について説明します。 # モデルに外部キーを設定する ## リレーションシップ 今回は1つのブログ記事は複数のコメントを持つ1対多のリレーションシップを例に説明します。現在は` [...]
2022年2月10日 14:18
【Rails】Capybaraのfill_inメソッドを実行すると「既存レコードの内容+指定した内容」がセットされる事象の原因と対処【RSpec】
# はじめに RSpec + Capybaraを使用して、Railsアプリの統合テストを実装しています。とあるモデルの編集画面において、入力フォームの内容を書き換えた上で送信し、レコードが更新されることを確認します。 入力フォームの内容を書 [...]
2022年1月27日 21:22
【Rails】GitHubのセキュリティアラートで発見された脆弱性を解消する方法
# はじめに GitHubにはセキュリティアラートという機能があります。セキュリティアラートはリポジトリに含まれるライブラリやパッケージの脆弱性を定期的にチェックし、脆弱性のあるライブラリやパッケージが発見されたらアラートで知らせてくれるという機 [...]
2022年1月16日 10:36