LoginSignup
13
16

More than 5 years have passed since last update.

Devise環境にDevise-Two-Factorを導入して2段階認証機能を実装する(1)

Posted at

概要

こんにちわ。セキュリティの実装、好きですか???

現在のプロジェクトでdeviseに2段階認証を組み込もうとしたのですが、ググってもdevise-two-factorについての日本語での導入記事がほぼ見つかりませんでした。
ですので今回、実装フローと個人的にハマったポイントについてまとめていきたいと思いました。
それではまず2段階認証の概要から。

そもそも2段階認証とは?

通常ログイン後、2段階認証をオンにしているユーザーにおいては、携帯端末を利用して生成したワンタイムパスワードを使用して、よりセキュアなログインとアカウント管理ができるシステムです。
現代サービスでのセキュリティ機能の需要は高まるばかりで、Google、Dropbox、エンジニア向けサービスで挙げるとGitHubやQiitaなどでも実装されています。

2段階認証に必要なフロー

  1. ユーザーごとにユニークなハッシュ値をもたせて、それをQRコード化したものをGoogleAuthenticatorで読み取ってもらう。
  2. GoogleAuthenticatorで生成したワンタイムパスワードと、サーバーが保持しているユーザーのワンタイムパスワードを比較して、二段階認証を有効化する。
  3. 次回ログインから、2段階認証がオンになっているユーザーは2段階認証専用のログイン画面に遷移させ、そこでワンタイムパスワードを認証させる。
  4. 端末を持っていないユーザーのためのバックアップコードを発行し、そちらからもログインできるようにする。

今回は1、2までのやり方をまとめてみようと思います。

deviseについて

plataformatec/devise
deviseはとても楽にサインアップ、ログイン、ログアウトなどのセッション管理の実装法を提供してくれるため、WEBサービスの構築に広く使用されています。

devise-two-factorについて

tinfoil/devise-two-factor
deviseですでにセッション管理がされている状態に、拡張する形で2段階認証を実装するgemです。
deviseをカスタムして使っている状態でも組み込むことができます。

実装

開発環境
Ruby 2.2.2
Rails 4.2.4
Vagrant 1.7.4
VirtualBox 5.0.0

Model

公式READMEに従って準備をしていきます。
deviseでの実装は終わっていることを前提に、話は進めていきます。

まずdevise-two-factorをインストールしましょう。

Gemfile
gem 'devise-two-factor'

今回自分がdeviseを適用しているのはUserモデルなので、コマンドの対象もUserモデルとなります。
Userモデルではない場合は、以下のコマンドすべてをUserから任意のモデル名に置き換えてください。

1. 暗号生成に必要な環境変数を設定、読み込ませる

まずdeviseでセッション管理の対象になっているモデル、今回はUserモデルですのでuser.rbのdeviseの設定に付け足す形で記述します。
ちなみにdeviseの管理対象がUserモデルでない場合も、そのモデルを指定してこれ以下のコマンド

user.rb
class User < ActiveRecord::Base
  devise :invitable, :registerable, :confirmable,
         :recoverable, :rememberable, :trackable, :validatable,
         ### add↓ ###
         :two_factor_authenticatable,
         otp_secret_encryption_key: ENV['FAVORITE_ENCRYPTION_KEY_NAME']

次に環境変数を作成、設定しておきましょう。名前はお好きなように。
Rubyで乱数を作って入れておけばいいので、rake secretで実行した値を入れておきましょう。
設定した後、念のためRailsコンソールを起動してENVコマンドで環境変数が読み込めているか確認しておきましょう。

※ここで環境変数がしっかり読み込めてないと、正しいハッシュ値が生成されません、注意しましょう(自分はここでハマりました...

.env.development
FAVORITE_ENCRYPTION_KEY_NAME=bc042f9c4558cb646209dcb102693015bd330e904d3594014cfbb21cabc5bae3444ceade713684452093a83a0a9f1b101fd5b9ecffdebc56b1e2fc8cd5445a5f

2. 暗号生成用の鍵を入れるカラムを作成する

$ rails generate devise_two_factor User FAVORITE_ENCRYPTION_KEY_NAME

これでユーザーごとにセキュアなワンタイムパスワードを提供するためのパラメータを格納するカラムと、ユーザーが2段階認証を有効化しているかどうかを判断するためのbool値を入れるカラムがusersテーブルに作成されます。

  • encrypted_otp_secret
  • encrypted_otp_secret_iv
  • encrypted_otp_secret_salt
  • otp_required_for_login (boolean)

ivの解説  初期化ベクトル - Wikipedia
saltの解説 ソルト(salt)とは | 日立ソリューションズの情報セキュリティブログ

3. deviseのストロングパラメータにワンタイムパスワード属性を追加する

otp_attemptは、email、passwordなどと一緒にsubmitする際に送る、ワンタイムパスワードパラメータです。
許可しておかないと、submit時に弾かれてしまいます。

application_controller.rb
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  def configure_permitted_parameters
    devise_parameter_sanitizer.for(:sign_in) << :otp_attempt
  end
  ##### Something...
end

以上でモデル側の設定は終了です。

Controller,View

devise-two-factorでは、認証のさせ方に決まった作法やフローはありません。
なので、いつ、どのURLでワンタイムパスワードを認証させるかを自分で考える必要があります。

今回はユーザープロフィールページからリンクで2段階認証有効化ページへ飛ぶと同時に、ハッシュ値が生成、保存され、QRコードを表示、という設計にしました。
showアクションでリソースの作成、変更をしているので、完全なRESTfulではなくなってしまっているのですが、今回の場合は綺麗にまとまるので良しとしましょう。

QRコードの作成に必要なgemです。インストールしておきましょう。

Gemfile
gem 'rqrcode'
two_factor_authentication_controller.rb
class TwoFactorAuthenticationsController < ApplicationController
  def show
    current_user.otp_secret = User.generate_otp_secret
    current_user.save!
  end

  def create
    if params[:otp].to_i == current_user.current_otp.to_i
      current_user.otp_required_for_login = true
      current_user.save!
      redirect_to root_path
    else
      render :show
    end
  end
end
application_helper.rb
def qrcode_tag(text, options = {})
  RQRCode::QRCode.new(text).as_svg(options).html_safe
end
show.html.haml
%h3 QRコードを読み取ってください
= qrcode_tag current_user.otp_provisioning_uri(current_user.email, issuer: "YourServiceName"), module_size: 4

%h3 ワンタイムパスワード(6桁)の入力
  = form_tag correct_path, method: 'post', class: 'form-inline' do
    %div.form-group
      = text_field_tag(:otp, nil, class: 'form-control', required: true, pattern: '^\d{6}$', maxlength: 6)
    = submit_tag '認証', class: 'btn btn-primary', 'data-disable-with' => '送信中...'

ハッシュ値をそのままQRコード化すると正しいQRコードが生成されないので、注意してください。
必ずユーザーのemailを与える必要があります。
ここで結構ハマりました...

これでshow.html.hamlにQRコードが表示されますので、それをスマホで落としたGoogleAuthenticatorで読み取れば30秒ごとに更新されるワンタイムパスワードが生成されます。その後フォームに入力した値をsubmitすれば、2段階認証の有効化に成功します。

フォームから送られたワンタイムパスワードが正しいかどうかは、createアクションで評価しています。
ワンタイムパスワードが合っていれば、otp_required_for_loginがtrueにされますので、次回のログインからこのユーザーに対して2段階認証フォームを経由させるように今後設計します。

2段階認証を解除させる機能は、otp_required_for_login = falseにするdestroyアクションを作成するだけで完了です。

※ここでもう一つハマりポイント。
VirtualBox内で作業している場合(だいたいがそうだと思うのですが)時刻が結構ずれます。ハッシュ生成アルゴリズムの中でタイムスタンプを使っているようで、特に仮想環境内でのサーバー時刻設定は開発環境でも定期的に設定しなおしましょう。
ちなみにGoogleAuthenticatorは端末内での時刻設定から時刻を取得しているので、仕事のために時計を5分ほど前にずらしていたりしているケースなどでも正しいワンタイムパスワードが生成できません。

まとめ

以上が、ユーザーに対して2段階認証を有効化させるまでの実装フローになります。
devise便利ですけど、ラップされてる分、内部構造がわかりにくいです。実際になかなか作業は難航しました。

次回は実際にdeviseログインとからめて2段階認証させるフローについて書いていこうと思います。
間違い等あれば、まさかり投げていただければ幸いです。

それでは〜〜〜:D

13
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
16