Railsアプリにdevise-two-factorとrqrcodeを使って2段階認証を導入した

こんにちは!kossyです!




最近はオリジナルアプリの実装に熱を入れ過ぎてブログの更新が疎かになっておりました、、、
アウトプットが大事だと頭でわかってはいるものの、
実行に移すのは大変ですね、、、





さて、今回はRailsアプリに2段階認証を導入する手順について、
ブログに残してみたいと思います。




環境
Rails 5.2.2
Ruby 2.5.1
devise 4.6.1
devise_two_factor 3.0.3
rqrcode 0.10.1
dotenv-rails 2.6.0
google_authenticator
macOs Mojave



[注意]
この解説記事内でgoogleの2段階認証アプリを使うので、実際に導入してみたい方は
お手持ちのスマートフォンGoogle Authenticatorを入れて下さい。
iOS: ‎「Google Authenticator」をApp Storeで
Android: Google 認証システムのインストール - Android - Google アカウント ヘルプ





Gemの導入

まずはGemの導入からです。

./Gemfile

# groupのブロック外ならどこに記述してもOKです

gem 'devise'
gem 'devise-two-factor'
gem 'rqrcode'
gem 'dotenv-rails'

# ターミナル
$ bundle install

それぞれ端的に解説すると、
deviseは言わずと知れた、認証の仕組みを数コマンドで提供してくれるGemです。
devise_two_factorを使うと、簡単に2段階認証の仕組みをRailsアプリに導入することができます。
rqrcodeは、QRコードの生成を行うgemです。
dotenv-rails環境変数の管理をしてくれるGemです。



deviseの導入

deviseの導入は細かい説明は省きます。
詳しく知りたい方は公式ドキュメントや、
[*Rails*] deviseの使い方(rails5版) - Qiita
こちらを参考にしてみてください。

$ rails g devise:install

$ rails g devise:views

$ rails g devise User

$ rails g devise:controllers users

$ rails db:migrate

上記のコマンドを順に実行してください。

次に、ルーティングを追加します。

config/routes.rb

Rails.application.routes.draw do
  devise_for :users, controllers: {
    registrations: 'users/registrations',
    sessions: 'users/sessions'
  }

end

これでdeviseの導入は終了です。

次にdevise_two_factorの設定を行います。




devise-two-factorを導入


ターミナルで以下のコマンドを実行します。

$ rails g devise_two_factor User TWO_FACTOR_ENCRYPTION_KEY

$ rails db:migrate

このコマンドを実行することで、以下の処理が実行されます。
・app/models/user.rbにdevise_two_factorの設定を追加
・:database_authenticatableを削除しようとする。 ※削除されない場合は手動で削除してください。
・config/initializers/devise.rbにwardenの設定を追加
・db/migrate下に二段階認証関連のカラムを追加するためのマイグレーションファイルを生成
追加されるカラムは以下の5種類です。

  • encrypted_otp_secret(string)
  • encrypted_otp_secret_iv(string)
  • encrypted_otp_secret_salt(string)
  • consumed_timestep(integer)
  • otp_required_for_login(boolean)

ワンタイムパスワードのパラメータを入れるカラムと、
ユーザーが2段階認証を有効にしているかどうかを判断するためのカラムを追加しています。





次に、環境変数の設定を追加します。



.envファイルの設定



アプリケーションフォルダ直下に.envファイルを作成し、
環境変数を設定します。

./ .env

# 例です
TWO_FACTOR_ENCRYPTION_KEY=hogehogehogehogehogehogehogehogehogehogehoge

このファイルは.gitignoreに追加して、gitの管理から外れるようにしましょう。

.gitignore

.env


次に、application_controller.rbにStrongParametersでotp_attempt(認証コード)を許可するように設定を加えます。


StrongParameterの設定

StrongParameterについては以下の記事が詳しかったです。
Rails初学者がつまずきやすい「ストロングパラメータの仕組み」

app/controllers/application_controller.rb



class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
  end

end

次項から、2段階認証の仕組みを実装していきます。



2段階認証を実装



Deviseを使っているRailsアプリに2段階認証を導入する - Qiita
こちらの記事のコードを参考に実装させていただきました。


app/controllers/users/sessions_controller.rbを編集し、通常のdeviseの認証の前に処理を挟むようにします。

class Users::SessionsController < Devise::SessionsController
  prepend_before_action :authenticate_with_two_factor, only: [:create]

  private
  def authenticate_with_two_factor
    # strong parameters
    user_params = params.require(:user).permit(:email, :password, :remember_me, :otp_attempt)

    if user_params[:email]
      user = User.find_by(email: user_params[:email])

    elsif user_params[:otp_attempt].present? && session[:otp_user_id]
      user = User.find(session[:otp_user_id])
    end
    self.resource = user

    return unless user && user.otp_required_for_login

    if user_params[:email]
      if user.valid_password?(user_params[:password])
        session[:otp_user_id] = user.id
        render 'devise/sessions/two_factor' and return
      end

    elsif user_params[:otp_attempt].present? && session[:otp_user_id]
      if user.validate_and_consume_otp!(user_params[:otp_attempt])
        session.delete(:otp_user_id)
        sign_in(user) and return
      else
        flash.now[:alert] = 'Invalid two-factor code.'
        render :two_factor and return
      end
    end
  end
end


次に、認証コードの入力画面を作成します。

app/views/devise/sessions/two_factor.html.erb


<h2>Log in</h2>

<%= form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| %>
  <div class="field">
    <%= f.label :otp_attempt %><br />
    <%= f.text_field :otp_attempt %>
  </div>

  <div class="actions">
    <%= f.submit "Verify" %>
  </div>
<% end %>

ルーティングは下記のように実装します。

resource :two_factor_auth, only: [:new, :create, :destroy]


次に、2段階認証周りの機能を提供するtwo_factor_auths_controller.rbを作成します。

app/controllers/two_factor_auths_controller.rb



class TwoFactorAuthsController < ApplicationController

  # 2段階認証有効化確認
  def new
    unless current_user.otp_secret
      current_user.otp_secret = User.generate_otp_secret(32)
      current_user.save!
    end

    @qr_code = build_qr_code
  end

  # 2段階認証有効化
  def create
    if current_user.validate_and_consume_otp!(params[:otp_attempt])
      current_user.otp_required_for_login = true
      current_user.save!

      redirect_to root_path

    else
      @error = 'Invalid pin code'
      @qr_code = build_qr_code

      render 'new'
    end
  end

  # 2段階認証無効化
  def destroy
    current_user.update_attributes(
      otp_required_for_login:    false,
      encrypted_otp_secret:      nil,
      encrypted_otp_secret_iv:   nil,
      encrypted_otp_secret_salt: nil,
    )
    redirect_to root_path
  end

  private
  
  def build_qr_code
    label = "service_name"
    # issuerでGoogle Authenticator上の登録サービス名として表示
    issuer ="company_name"
    uri = current_user.otp_provisioning_uri(label, issuer: issuer)
    qrcode = RQRCode::QRCode.new(uri)
    qrcode.as_svg(
      offset: 0,
      color: '000',
      shape_rendering: 'crispEdges',
      module_size: 2
    )
  end
end






QRコード表示画面を作成

app/views/two_factor_auths/new.html.erb



<h1>Enable 2FA</h1>

<% if @error %>
  <div><%= @error %></div>
<% end %>

<div><%= raw @qr_code %></div>

<%= form_tag two_factor_auth_path do %>
  <div class="field">
    <%= label_tag :otp_attempt %><br />
    <%= text_field_tag :otp_attempt %>
  </div>

  <%= submit_tag :submit %>
<% end %>


認証機能の有効化・無効化画面の例

app/views/home/index.html.erb(ユーザープロフィール画面があればそこに表示するのがベスト?)


<div>
  <% if user_signed_in? %>
    <% if current_user.otp_required_for_login %>
      <%= button_to "Disable 2FA", two_factor_auth_path, method: :delete %>
    <% else %>
      <%= button_to "Enable 2FA", new_two_factor_auth_path, method: :get %>
    <% end %>
  <% end %>
</div>


これで一通りの実装は完了です。
それでは、ローカルで試してみます。(本ブログでは解説していないcontrollerとrootの設定をしています。)





localhost:3000/users/sign_upにアクセスし、必要情報を入力して、submitを押下します。
f:id:kossy-web-engineer:20190220220517p:plain




ユーザー登録に成功すると、enable 2FAのボタンが表示されるので、クリックします。
f:id:kossy-web-engineer:20190220220608p:plain




すると、QRコードが表示されるので、google認証のアプリでQRコードを読み取り、6桁の認証コードを入力します。
f:id:kossy-web-engineer:20190220220754p:plain




正しく認証をパスすると、Enable 2FAだったボタンが、Disable 2FAの表示に変わっていると思います。
f:id:kossy-web-engineer:20190220220854p:plain




これで2段階認証の導入は終了です。
思った通り動作するのはやっぱり嬉しいですね。




参考にさせていただいた記事
Deviseを使っているRailsアプリに2段階認証を導入する - Qiita
deviseとGoogle Authenticatorを用いてRailsシステムに「二段階認証」を導入した話 - LiBz Tech Blog
Railsの二段階認証Gem Devise-Two-FactorのDemoを触ってみた - Qiita
GitHub - tinfoil/devise-two-factor: Barebones two-factor authentication with Devise