Devise環境にDevise-Two-Factorを導入して２段階認証機能を実装する（１）

# 概要

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

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

### そもそも2段階認証とは？

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

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

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

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

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

### devise-two-factorについて
[tinfoil/devise-two-factor](https://github.com/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をインストールしましょう。

```rb:Gemfile
gem 'devise-two-factor'
```

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

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

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

```rb: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:.env.development
FAVORITE_ENCRYPTION_KEY_NAME=bc042f9c4558cb646209dcb102693015bd330e904d3594014cfbb21cabc5bae3444ceade713684452093a83a0a9f1b101fd5b9ecffdebc56b1e2fc8cd5445a5f
```

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


`$ 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](https://ja.wikipedia.org/wiki/%E5%88%9D%E6%9C%9F%E5%8C%96%E3%83%99%E3%82%AF%E3%83%88%E3%83%AB)
saltの解説　[ソルト(salt)とは | 日立ソリューションズの情報セキュリティブログ](http://securityblog.jp/words/6452.html)



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

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

```rb: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でワンタイムパスワードを認証させるかを自分で考える必要があります。

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

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

```rb:Gemfile
gem 'rqrcode'
```

```rb: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
```

```rb:application_helper.rb
def qrcode_tag(text, options = {})
  RQRCode::QRCode.new(text).as_svg(options).html_safe
end
```

```rb: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にされますので、次回のログインからこのユーザーに対して２段階認証フォームを経由させるように今後設計します。

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

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

# まとめ

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

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

それでは〜〜〜:D