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

はじめに

昨今はIDとパスワードによる認証だけでなく、ワンタイムパスワードによる2段階認証を導入するWebアプリが増えてきました。Railsで作成したWebアプリでも、IDとパスワードによる認証に加えて2段階認証を導入するニーズが高まっています。

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

実装の前提条件

認証機能の実装

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

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

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

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

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

2段階認証の実装

インストール

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

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

Gemfile

gem 'rotp'
gem 'rqrcode'

「rotp」用のカラムの追加

認証に使用しているモデルに「rotp」用のカラムを追加します。

以下のコマンドを実行してマイグレーションファイルを作成します。マイグレーションファイルはdb/migrate/ディレクトリ配下に作成されます。

$ rails generate migration add_otp_secret_to_users

作成したマイグレーションファイルに以下を追記します。

add_otp_secret_to_users.rb

class AddOtpSecretToUsers < ActiveRecord::Migration[6.1]
  def change
    # 以下を追記
    add_column :users, :otp_secret, :string
    add_column :users, :last_otp_at, :integer
  end
end
カラム 説明
otp_secret String Railsアプリとトークンソフトウェアで共有する32桁のシークレット文字列。
last_otp_at Integer 最後にワンタイムパスワードによる認証を行った日時を表す数字。

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
    # 以下を追記
    @otp_secret = ROTP::Base32.random
    totp = ROTP::TOTP.new(
      @otp_secret, issuer: 'YourAppName'
    )
    @qr_code = RQRCode::QRCode
      .new(totp.provisioning_uri(current_user.email))
      .as_png(resize_exactly_to: 200)
      .to_data_url
  end

  def create
  end
end

「rotp」のAPIを使ってランダムな32桁の文字列を生成し、シークレット文字列を作成します(@otp_secret)。シークレット文字列およびアプリ名を使って時間ベースのワンタイムパスワード(Time-based OTP's)を作成します(totp)。さらに、そのワンタイムパスワードを使ってQRコードを作成します(@qr_code)。

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

new.html.erb

<h2>2-Step Verification</h2>

<%= form_tag otp_secrets_path, method: :post do %>
  <%= hidden_field_tag :otp_secret, @otp_secret %>

  <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
    # 以下を追記
    @otp_secret = params[:otp_secret]
    totp = ROTP::TOTP.new(
      @otp_secret, issuer: 'YourAppName'
    )

    last_otp_at = totp.verify(
      params[:otp_attempt], drift_behind: 15
    )

    if last_otp_at
      current_user.update(
        otp_secret: @otp_secret, last_otp_at: last_otp_at
      )
      redirect_to(
        root_path,
        notice: 'Successfully configured OTP protection for your account'
      )
    else
      @qr_code = RQRCode::QRCode
        .new(totp.provisioning_uri(current_user.email))
        .as_png(resize_exactly_to: 200)
        .to_data_url
      flash.now[:alert] = 'The code you provided was invalid!'
      render :new
    end
  end
end

シークレット文字列およびアプリ名を元に時間ベースのワンタイムパスワードを作成し、そのワンタイムパスワードとユーザーが入力したワンタイムパスワード(フォームから送信されたワンタイムパスワード)が一致しているか認証を行います。認証がOKの場合は認証した日時を表す数字が返され、NGの場合はnilが返されます。

認証結果が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.otp_secret
=> "MIHG26U357IEGHPLHJB5YEYOVXRK3RJJ"
> user.last_otp_at
=> 1637998860

ユーザーのシークレット文字列および最終認証日時を表す数字がちゃんと設定されています。2段階認証の有効化状態はシークレット文字列の有無で判定し、シークレット文字列および最終認証日時を表す数字を初期化することで2段階認証を無効化することができます。

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])
    totp = ROTP::TOTP.new(
      user.otp_secret, issuer: 'YourAppName'
    )
    last_otp_at = totp.verify(
      params[:otp_attempt], after: user.last_otp_at, drift_behind: 15
    )
    if last_otp_at
      user.update(last_otp_at: last_otp_at)
      sign_in(user)
      redirect_to(
        root_path,
        notice: 'Signed in successfully.'
      )
    else
      flash.now[:alert] = 'The code you provided was invalid!'
      render :new_two_step_verification
    end
  end
end

実装内容は「2段階認証の有効化:有効化機能の実装」セクションとほとんど同じです。シークレット文字列およびアプリ名を元に時間ベースのワンタイムパスワードを作成し、そのワンタイムパスワードとユーザーが入力したワンタイムパスワード(フォームから送信されたワンタイムパスワード)が一致しているか認証を行います。認証がOKの場合は認証した日時を表す数字が返され、NGの場合はnilが返されます。

認証結果が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段階認証の認証画面でワンタイムパスワードを入力し、送信ボタンをクリックすると、ログインが成功することを確認します。また、適切でないワンタイムパスワードを入力してログインが失敗することも確認します。

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

まとめ

本来、2段階認証はユーザーが任意のタイミングで有効化/無効化を切り替えられる構成にするのが望ましいです。2段階認証を無効化する機能については、データベースのシークレット文字列および最終認証日時の値を初期化するなどの実装が考えられます。また、データベースの情報が漏洩した場合を考慮して、シークレット文字列は暗号化することを検討したほうがいいでしょう。

最近では、IDとパスワードによる認証とワンタイムパスワードによる認証の2段階認証ではなく、メールアドレスとワンタイムパスワードの認証のみというWebアプリも増えています。メールアドレスとワンタイムパスワードによる認証機能は、シンプルながらも堅牢なセキュリティ的が確保できる上、実装が(従来の2段階認証と比べて)楽というメリットもあります。

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

関連記事

【Rails】Paranoiaを使用した論理削除(ソフトデリート)
# はじめに Paranoiaは、Railsアプリケーションで論理削除(ソフトデリート)を実現するためのGemです。 論理削除は、データベースのレコードを物理的に削除するのではなく、削除フラグを設定することで「削除済み」とみなす方法です。こ [...]
2024年7月20日 21:33
【Rails】activerecord-multi-tenantを使用したマルチテナントアプリケーションの作成
# はじめに マルチテナントアプリケーションでは、複数の顧客(テナント)が同じアプリケーションを利用するため、データの分離が必要です。 activerecord-multi-tenantは、このようなマルチテナント環境をサポートするための便 [...]
2024年7月18日 16:50
【Rails】RubyとRailsにおけるattr_reader, attr_writer, attr_accessorの概念と使用方法
# はじめに RubyとRailsの開発において、`attr_reader`,`attr_writer`,`attr_accessor`は非常に便利なメソッドです。これらは、クラス内でインスタンス変数に対するゲッターおよびセッターメソッドを簡単に [...]
2024年7月17日 18:11
【Rails】RubyとRailsにおけるyieldの概念と使用方法
# はじめに RubyとRailsにおける`yield`は、メソッドやテンプレートの中で動的にコードブロックを実行する能力を提供し、これによってコードの再利用性と拡張性が大幅に向上します。本記事では、RubyとRailsにおける`yield`の概 [...]
2024年7月17日 13:15
【Rails】AASMを使用してオブジェクトの状態遷移を効率的に管理
# はじめに Railsアプリケーションにおいて、オブジェクトの状態管理は重要な課題の一つです。AASM (Acts As State Machine) gemは、複雑な状態遷移を効率的に管理します。本記事では、AASMの基本的な使い方を解説して [...]
2024年7月16日 18:00
【Rails】RSpec + Swagger + rswagでアプリケーションのAPIをテストおよびドキュメント化する方法
# はじめに Railsアプリケーションの開発において、APIのテストとドキュメント化は重要な要素です。 RSpecはテストフレームワークとして広く利用されており、SwaggerはAPIの設計とドキュメント化を支援します。これらを統合するr [...]
2024年7月16日 14:27
【Rails】mailcatcherを使用して開発環境でメール送信をテストする方法
# はじめに mailcatcherは、開発環境でのメール送信をキャプチャするためのツールです。ローカルで送信されたメールをブラウザ上で簡単に確認できるようにします。mailcatcherをRailsアプリケーションで使用する方法について説明しま [...]
2024年7月15日 16:37
【Rails】impressionistを使用してページビューやクリック数を追跡する方法
# はじめに impressionist Gemを使用してRailsアプリケーションでページビューやクリック数を追跡する方法について説明します。 # 実装方法 ## impressionist Gemのインストール まず、impre [...]
2024年7月15日 14:18