🌟

devise-two-factorで二要素認証を実装してみよう!!

に公開

はじめに

こんにちは、ラブグラフでエンジニアインターンをしているうらっしゅです!
今回は、devise-two-factorという二要素認証を提供するgemの紹介になります!
一見難しそうに見えるこの機能ですが、今回紹介するgemを用いるとかなり簡単でかつ直感的に実装することができます。

devise-two-factorとは

devise-two-factorは、Ruby on Railsアプリケーションにおける二要素認証を簡単に実装するためのgemです。Google Authenticatorなどの認証アプリを使用して、ワンタイムパスワード(OTP)を生成し、そのOTPによる認証を行うことができます。
※ 以下、ワンタイムパスワードはOTPと表記する。

主要な機能としては以下の2つが挙げられます:

  1. プロビジョニングURIの生成

    • QRコードを生成するためのURIを生成し提供します。これにより、ユーザーは認証アプリでQRコードをスキャンすることで容易に二要素認証を設定できます。
  2. OTPの検証

    • 認証アプリによって生成されたOTPを検証します。ユーザーのログイン時に、通常のパスワードに加えてOTPの入力が必要となるため、セキュリティが向上します。

このようにして、devise-two-factorを使用することでアプリケーションのセキュリティを強化し、ユーザーエクスペリエンスを向上させることができます。

https://github.com/devise-two-factor/devise-two-factor/tree/v4.x

想定するユーザの動線

二要素認証の登録フロー
二要素認証による認証フロー

必要なカラムの定義や諸設定

devise-two-factorを扱う上で必要なカラムの定義や必要な環境変数を紹介していきたいと思います。

必要なGemfile

v4.1.0にしている理由は私のRailsが6系なためです。

gem 'devise-two-factor', '4.1.0'
必要なカラムの定義
  • encrypted_otp_secret : 2要素認証で使用する秘密鍵が暗号化されたもの
  • encrypted_otp_secret_iv : 2要素認証の秘密鍵の暗号化プロセスに使用される初期化ベクトル
  • encrypted_otp_secret_salt : 2要素認証の秘密鍵の暗号化プロセスに使用される追加データ(ソルト)
  • consumed_timestep : 時間に基づいて動的に変化するワンタイムパスワード(OTP)の生成に使用されるタイムステップ
  • otp_required_for_login : ログイン時に2要素認証が必要かどうか (このカラムで二要素認証の有効化か無効化の状態を管理しています。)
Schemafile
create_table :users do |t|
    t.string   :encrypted_otp_secret
    t.string   :encrypted_otp_secret_iv
    t.string   :encrypted_otp_secret_salt
    t.integer  :consumed_timestep
    t.boolean  :otp_required_for_login, null: false, default: false
end
必要な環境変数の定義

環境変数のOTP_SECRET_ENCRYPTION_KEYに適当な文字列を設定すれば良いです。

OTP_SECRET_ENCRYPTION_KEY=

Model部分の実装

それでは、Userモデルの二要素認証の実装についてみていきましょう!!
秘密鍵の設定はあるものの、必要な機能はプロビジョニングURIの生成認証コードの検証の2つのみなシンプルなものです。

※ 説明用にUserモデルに主要機能を書きました。

user.rb
class User < ApplicationRecord
    # この辺りは通常のdeviceの振る舞いを提供しています(今回はそこまで関係ない)
    devise :rememberable, :validatable, :recoverable, :confirmable

    # モデルに二要素認証の振る舞いを提供し、DBに秘密鍵を暗号化する(`generate_otp_secret`メソッド)時にotp_secret_encryption_keyが使われます。
    devise :two_factor_authenticatable, otp_secret_encryption_key: ENV.fetch("OTP_SECRET_ENCRYPTION_KEY", nil)

    # 二要素認証の設定URIを提供する(otp_secretが変更されない限り同じURI)
    def two_factor_provisioning_uri
        # 二要素認証の秘密鍵が未設定なら、秘密鍵を生成し暗号化して保存されている。
        if self.otp_secret.nil?
            self.otp_secret = User.generate_otp_secret
            self.save!
        end
        label = "Lovegraph:#{self.email}"
        self.otp_provisioning_uri(label, issuer: "Lovegraph", otp_secret: self.otp_secret)
    end

    # 認証コードの検証と消費を行うメソッド
    def two_factor_verify_and_consume_code(input_code)
        self.validate_and_consume_otp!(input_code)
    end
end

Controller部分の実装

次に、Controller側のメイン実装をみていきましょう!!
かなり抜粋していますが二要素認証を登録するフローが見えてくると思います。Userモデルで主要機能は抑えているのでControllerはそれを呼び出したりフォーマットの検証, ルーティング周りの実装がメインです。

two_factors_controller.rb
class TwoFactorsController < ApplicationController
  def show
  end

  # 二要素認証の設定前パスワード入力画面
  def input_password
  end

  # 二要素認証を設定するためのパスワード確認
  def password_confirm
    password = params[:password]

    if !current_user.valid_password?(password)
      @login_failed_reason = "パスワードが間違っています。"
      return render :input_password
    end

    session[:passed_password] = true
    redirect_to qr_code_two_factor_path
  end

  # 二要素認証のURIを元にQRコードを表示する画面
  def qr_code
    redirect_to input_password_two_factor_path, alert: "パスワードを入力してください" if session[:passed_password].blank?

    @otp_provisioning_uri = current_user.two_factor_provisioning_uri
  end

  # OTPのコード入力画面
  def input_code
    redirect_to input_password_two_factor_path, alert: "パスワードを入力してください" if session[:passed_password].blank?
  end


  def verify_two_factor_code
    redirect_to input_password_two_factor_path, alert: "パスワードを入力してください" if session[:passed_password].blank?

    input_code = params[:two_factor_code]

    if !input_code.match?(/\A\d{6}\z/)
      @error_message = "認証コードは6桁の数字で入力してください。"
      return render :input_code
    end

    if !current_user.two_factor_verify_and_consume_code(input_code)
      @error_message = "認証コードが間違っています。"
      return render :input_code
    end

    current_user.update!(otp_required_for_login: true)

    session[:passed_password] = nil
    redirect_to two_factor_path, notice: "二要素認証を有効化しました。"
  end
end

Strategies::TwoFactorAuthenticatableを使う選択

実は、devise-two-factorにはもう一つ、もっとシンプルな実装方法があります。
それが、Strategies::TwoFactorAuthenticatableを使った方法です。

この方法を使うと、emailpassword、そして認証コードを1回のリクエストでまとめて検証することができ、認証処理を非常にスリムに保つことができます。

しかしながら、今回紹介している「パスワードを先に検証し、その後に認証コードを入力させる」というフローは、この方法では実現できません。
Strategies::TwoFactorAuthenticatableを使うと、最初からすべての情報が揃っている前提で認証を行うため、段階的な認証プロセスを設計することが難しくなるからです。

まとめると、

  • パスワード検証 → 認証コード入力2段階で進めたい場合 → 本記事で紹介している方法がおすすめ
  • パスワード・認証コードを同時にまとめて入力するUIでも問題ない場合Strategies::TwoFactorAuthenticatableの活用を検討するとよりシンプルに実装可能

という整理になります!

アプリケーションの要件やUXに合わせて、どちらのアプローチを取るか選んでみてください。

注意点

  • two_factor_authenticatableを使うと、一度使ったOTPは無効になるため、リトライ時のUX設計に注意しましょう。
  • 二要素認証の設定前には、必ずパスワード確認を挟むようにしましょう。セッション(session[:passed_password])管理が甘いと、認証をすり抜けられる危険があります。
  • スマホを紛失した場合など、二要素認証ができない状態になったときの「リカバリー手段(サポートに連絡してもらうなど)」も別途用意しておきましょう。(ここでは紹介しませんでしたが、リカバリーコードも実装することができます)
  • 通常ログイン後、otp_required_for_loginがtrueのユーザーに対しては、追加でOTP認証を求めるフローを忘れずに実装しましょう。(認証フローの分岐が必要です)

まとめ

今回は、Railsアプリケーションに簡単に二要素認証を組み込めるdevise-two-factorについて紹介しました。
ポイントをおさらいすると、

  • 必要なカラムや環境変数を整えるだけで導入できる
  • モデル側はプロビジョニングURI生成OTP検証だけ実装すればよい(説明のためにモデルに実装しましたが、方針によってはわざわざメソッドをラップする必要もないです)
  • コントローラ側ではパスワード確認QRコード生成OTP検証のフローを組み立てる

という非常にシンプルな形で二要素認証が実現できます。

Rails標準のdeviseと自然に連携できるため、ユーザー体験を損なわずにセキュリティを強化したい場合に非常におすすめです!

興味がある方はぜひ実装にチャレンジしてみてください!🚀

ラブグラフのエンジニアブログ

Discussion