LiBz Tech Blog

LiBの開発者ブログ

deviseとGoogle Authenticatorを用いてRailsシステムに「二段階認証」を導入した話

目次

はじめに

はじめまして! 株式会社LiBの内定者インターンをしている江田です。

LiBでは2018年の2月にエンジニアとして内定をいただきました。

2019年卒から新卒採用がスタートしたので、内定者以外は全員中途社員です。

周りを見渡せば、素晴らしい経歴を持つ猛者ばかりの環境で、単位も取得し終わった私は学生と社会人の狭間で日々コードを書いています。

ちなみに、私は大学では法学部法律学科に身を置いていたので、授業ではテクノロジーに関する題材は 一切登場しなかったです。

いつかの知財法の授業で何故かSpotifyが出てきたような無いような。

なぜ自分がLiBを選択したのかは今後お話しするとして、早速本題に移っていきます。

セキュリティ対策しようぜ

さて、今回紹介するのは社内の管理システムに二段階認証を導入した事例です。

LiBではLiBzCAREERという女性向け転職支援サービスの開発を担当しています。

データの流出はなんとしても避けたいということで、通常のログイン認証に加えて、もう一段階セキュリティのレベルを上げましょう。

Railsで二段階認証を導入する

二段階認証の実装の説明に入る前に、ある日の出来事を紹介します。

ある朝、普段どおり会社に行くと、隣の席に座っているCTOから一言。

「エディー(自分のアダ名)次のタスクは二段階認証ね。よろしくー。」

「あっ、はい。」

その日から、セキュリティレベル向上の施策としてみんな大好き二段階認証をシステムに組み込むことに。

「はいはい、二段階認証ね、アレね、アレアレ....あれ??」

正直、二段階認証を導入するに当たって、攻略の糸口が全く思い浮かびませんでした。

しかし、弊社にはヤフーやクックパッドでの開発を経て、ビットジャーニーで代表を務める技術顧問の井原さんがいる!ということで意気揚々と質問しようとしましたが、そんな野望を打ち砕くツイートが。

タイミングが良いのか悪いのかまさに井原さんも二段階認証の実装が分かる相手を探していました....w

これは普段からお世話になってる井原さんに恩を返すチャンスだと思い、二段階認証実装の旅に出掛けました。

技術選定

当初、途方に暮れていたとはいえヒントが無かったわけではありません。

弊社のシステムのうち管理画面は gem 'devise' を導入しています。

この時点で、Railsなんだからdeviseに基づく二段階認証のgemの1つや2つあるだろうと思い、サーチしてみることに。

f:id:edenden:20181106184019p:plain

いろいろ調べてみた結果、 gem 'devise-two-factor' を使用することにしました。

背景としては下記のような事項が挙げられます。

  • deviseに基づいたRailsエンジンである。
  • SMSで6桁の番号を送信するパターンだとキャリア回線を使うので契約及び費用が発生する。
    • したがって、無料で使用できるQRコード表示のソフトウェアを使用したい。
  • トークンソフトウェアの場合は、スマホに専用アプリ(例:Google Authenticator)をインストールする必要があるものの、実装難易度が下がる。
  • Rails5.2.0までバージョン対応されている。

f:id:edenden:20181106184646p:plain

いざ実装

今回の実装は管理画面を扱うLiBの内部の社員に対して適用するものでした。 GitHubや各種SNSのように設定画面から個人が任意で二段階認証の設定を行う仕様ではなく、通常ログインの「ID/PASS入力」の後に強制的に二段階認証を実施するような実装にする必要がありました。 さらに、スマホ紛失時やうっかりアプリ消去などを見越して二段階認証を一旦取り消す機能も求められました。

流れのイメージは下の図のようになります。至ってシンプルですね。 f:id:edenden:20181106190019p:plain

[実装前準備]deviseのインストール

実装に取り掛かる前に、deviseをインストールして設定を終わらせておきます。

github.com

必要なgemの導入とモデル側の設定

次に、2つのgemを導入します。

github.com

github.com

gem 'devise-two-factor' # 認証用
gem 'rqrcode'           # QRコード生成用
$ bundle install

また本実装では暗号化キーを生成するため、予め環境変数の設定を行う必要があります。 モデルに記述を加えておきましょう。

devise :two_factor_authenticatable,
         :otp_secret_encryption_key => ENV['YOUR_ENCRYPTION_KEY']

そして、次のコマンドを打ち込みます。

$ rails generate devise_two_factor MODEL YOUR_ENCRYPTION_KEY

MODELには任意のモデル名

YOUR_ENCRYPTION_KEYには環境変数名を指定しましょう。

このコマンドによって下記の設定が実行されます。

  • app/models/MODEL.rbのdeviseディレクティブ調整
  • :database_authenticatableを削除しようとする。
    • 削除されない場合は手動で削除してください。
  • deviseの設定ファイルを調整
  • wardenの設定
  • MODELに二段階認証関連のカラムを追加するためのマイグレーションファイルを生成
    • encrypted_otp_secret(string)
    • encrypted_otp_secret_iv(string)
    • encrypted_otp_secret_salt(string)
    • consumed_timestep(integer)
    • otp_required_for_login(boolean)

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

最後に、deviseのストロングパラメータにemail、passwordなどと一緒にsubmitする際に送る、otp_attemptというワンタイムパスワードパラメータを追加します。

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

  # 中略

  private
  # ワンタイムパスワードのパラメーターを追加
  def configure_permitted_parameters
    devise_parameter_sanitizer.for(:sign_in) << :otp_attempt
  end
end

ここまでで、gemの導入とモデル側の設定が終了しました。

コントローラーとビュー

ID/PASS入力画面から認証コード入力画面に遷移する際に、当初はパラメーターからidを取得するような設計をしていました。 しかし、正式にログイン認証が完了していない時点でidがURLに表示されるのは、セキュリティホールを生む原因となるため、パラメータでidが送られないような設計に変更しました。

一方で、ID/PASS入力後にログインしようとしているユーザーの情報を引き継ぐ必要があります。 そこで、ユーザーIDをセッションに保持しておく作戦を採用しました。

class SessionsController < Devise::SessionsController

  # deviseのsign_inを扱うcreateアクションの前に挿入
  prepend_before_action :authenticate_with_two_factor, only: :create

  private

    # IDとPASSの認証が通った後に、two_factor_auths_controller.rbに二段階認証に処理を渡す。
    def authenticate_with_two_factor
      user = User.find_by(email: params[:user][:email])
      return if user.blank?
      return unless user.valid_password?(params[:user][:password])
      session[:user_id] = user.id
      redirect_to new_two_factor_auth_path
    end
end

ID/PASSの認証通過後の処理は実はシンプルで、ざっくり2つに分かれます。 1つは、ハッシュ値の生成と保存。 もう一方は、二段階認証を行うための入り口であるQRコードの表示です。

ハッシュ値の保存と生成に関しては、公式のREADMEにも記載がある通り、たった3行で完結です。 otp_required_for_loginはdeviseo-two-factorをgenerateした際に生成されたカラムで、有効化済みか否かを判別します。

current_user.otp_required_for_login = true
current_user.otp_secret = User.generate_otp_secret
current_user.save!

次にQRコードの表示ですが、事前にインストールしていたrqrcodeを用いて実装を進めていきます。 READMEには、

you'll need to generate a URI to act as the source for the QR code. This can be done using the User#otp_provisioning_uri method.

issuer = 'Your App'
label = "#{issuer}:#{current_user.email}"

current_user.otp_provisioning_uri(label, issuer: issuer)

# > "otpauth://totp/Your%20App:user@example.com?secret=[otp_secret]&issuer=Your+App"

とあります。 例えば、issuerをLiBz Tech Blog、labelをyour_nameにしてみます。 結果が分かったほうが良いと思うので、QRコード表示とGoogle Authenticatorで読み取った結果画面を表示してみます。 ※お手元にアプリがあれば実際に読み取ることも可能です。

@管理画面

f:id:edenden:20181107102829p:plain:w300

@スマホ(Google Authenticator)

f:id:edenden:20181107102737j:plain:w300

30秒ごとに更新されるワンタイムパスワードが生成されています。

実装ではissuerをサービスの名称にしたり、labelをユーザー名やユーザーのemailにするのが良いと思います。

ここまでで、二段階認証のQRコード表示までが完了しました。 実は、最もシンプルに実装する場合、もう既に必要な素材は揃っています。

残るはサービス独自のオリジナルな実装やエラーハンドリングが求められます。 参考までに今回の実装を踏まえたコードを掲載しておきます。

class TwoFactorAuthsController < ApplicationController

  skip_before_action :authenticate_user!, only: [:new, :create]

  # 2段階認証有効化のための事前手続き
  # QRコードを表示して、専用アプリで読み取る
  def new
    @user = User.find(session[:otp_user_id]).decorate
    return if @user.otp_secret && @user.otp_required_for_login
    # この時点で認証コードのシークレットキーを生成して保存しておく
    @user.update!(otp_secret: User.generate_otp_secret)
  end

  # 入力された6桁の数字が有効かチェック
  def create
    @user = User.find(session[:otp_user_id]).decorate
    if @user.validate_and_consume_otp!(params[:otp_attempt])
      unless @user.otp_required_for_login
        # 2段階認証有効化済みフラグを立てる
        @user.update!(otp_required_for_login: true)
      end
      session.delete(:otp_user_id)
      # 認証済みのユーザーのサインインをするDeviseのメソッド
      sign_in(@user)
      redirect_to root_path, notice: 'ログインしました'
    # 認証が通らなかった場合、再度QRコードもしくは、認証コード入力画面をレンダリング
    else
      @error = '認証コードが誤っているため、認証に失敗しました'
      render :new
    end
  end
end
// 二段階認証の有効化が未完了であれば、QRコードを表示する
- unless @user.otp_required_for_login
  h5
    | QRコードを読み取ってください
  == @user.build_qr_code

= form_tag two_factor_auth_path do
  .form-group
    = label_tag :otp_attempt, "認証コード"
    br
    = text_field_tag(:otp_attempt, nil, class: 'form-control', required: true, pattern: '^\d{6}$', maxlength: 6)
  = submit_tag '認証', class: 'btn btn-primary', 'data-disable-with' => '送信中...'

QRコードの表示はDecorator層に記述しました。

class AdminUserDecorator < Draper::Decorator

  delegate_all

  # 二段階認証用にQRコードを作成
  def build_qr_code
    # issuerがGoogle Authenticator上の登録サービス名として表示される
    label = "your_name"
    issuer = "LiBz Tech Blog"
    uri = otp_provisioning_uri(label, issuer: issuer)
    qrcode = RQRCode::QRCode.new(uri)
    # svg形式で表示
    qrcode.as_svg(
      offset: 0,
      color: '000',
      shape_rendering: 'crispEdges',
      module_size: 2
    )
  end
end

おわりに

途中で技術顧問の井原さんからペアプロをしていただいたお蔭で、なんとかリリースに漕ぎ着けました。Kibelaへの二段階認証の組み込みも任せてください!

普段からLiBもお世話になってる井原さんが制作したKibelaはこちら

ただ、エンジニアメンバーからのレビュー時に、多々コメントを貰ったので反省材料も多々あります(合計65コメントになりましたw)

普通にRailsWayの話やRESTfulな設計をするという段階での指摘もあり、まだまだ修行を積む必要があるな〜感じました。

f:id:edenden:20181106190345p:plain

まだ入社まで半年ありますが、文系・未経験からエンジニアとしてのキャリアがスタートするので、実務が初めてのRailsエンジニアに向けたエントリーもいつか書けたらと思います。

おわり!