概要
こんにちわ。セキュリティの実装、好きですか???
現在のプロジェクトでdeviseに2段階認証を組み込もうとしたのですが、ググってもdevise-two-factorについての日本語での導入記事がほぼ見つかりませんでした。
ですので今回、実装フローと個人的にハマったポイントについてまとめていきたいと思いました。
それではまず2段階認証の概要から。
そもそも2段階認証とは?
通常ログイン後、2段階認証をオンにしているユーザーにおいては、携帯端末を利用して生成したワンタイムパスワードを使用して、よりセキュアなログインとアカウント管理ができるシステムです。
現代サービスでのセキュリティ機能の需要は高まるばかりで、Google、Dropbox、エンジニア向けサービスで挙げるとGitHubやQiitaなどでも実装されています。
2段階認証に必要なフロー
- ユーザーごとにユニークなハッシュ値をもたせて、それをQRコード化したものをGoogleAuthenticatorで読み取ってもらう。
- GoogleAuthenticatorで生成したワンタイムパスワードと、サーバーが保持しているユーザーのワンタイムパスワードを比較して、二段階認証を有効化する。
- 次回ログインから、2段階認証がオンになっているユーザーは2段階認証専用のログイン画面に遷移させ、そこでワンタイムパスワードを認証させる。
- 端末を持っていないユーザーのためのバックアップコードを発行し、そちらからもログインできるようにする。
今回は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をインストールしましょう。
gem 'devise-two-factor'
今回自分がdeviseを適用しているのはUserモデルなので、コマンドの対象もUserモデルとなります。
Userモデルではない場合は、以下のコマンドすべてをUserから任意のモデル名に置き換えてください。
1. 暗号生成に必要な環境変数を設定、読み込ませる
まずdeviseでセッション管理の対象になっているモデル、今回はUserモデルですのでuser.rb
のdeviseの設定に付け足す形で記述します。
ちなみにdeviseの管理対象がUserモデルでない場合も、そのモデルを指定してこれ以下のコマンド
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
コマンドで環境変数が読み込めているか確認しておきましょう。
※ここで環境変数がしっかり読み込めてないと、正しいハッシュ値が生成されません、注意しましょう(自分はここでハマりました...
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時に弾かれてしまいます。
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です。インストールしておきましょう。
gem 'rqrcode'
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
def qrcode_tag(text, options = {})
RQRCode::QRCode.new(text).as_svg(options).html_safe
end
%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