Posted at

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

More than 3 years have passed since last update.


概要

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

現在のプロジェクトで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