TL;DR (概要)
こんにちは、スタメンエンジニアの井本です。普段はRuby on RailsやAWSなどサーバーサイド寄りの技術を用いて開発しています。最近はフィーチャーチーム体制に切り替わったこともあり、React入門中です。
さて今年の1月下旬、弊サービスTUNAGにて、新機能として「2要素認証」をリリースしました。 TUNAGバックエンドはRuby on Railsを用いて開発されていることもあり、認証機能はDeviseを用いて実装しています。 今回は元々のパスワード認証に加えて、OTP(One Time Password)認証をあわせて行うことで、2要素認証としました。 新しく追加するOTP認証をDeviseの設計に沿った形で実装しました。
本記事では、Deviseの実装の解説と、実装に則った新しい認証方法の追加について、その他Tips含めてお話しします。
※Deviseで認証するモデルがUserモデルである前提で進めます。
Deviseにおける認証ロジックの実装
DeviseはRackの認証ミドルウェアであるWarden上に実装されています。Wardenは認証ロジックをストラテジーパターンで置換可能なものとしており、Deviseはパスワード認証をWardenのストラテジーとして実装しています。またRememberableを有効化した場合のRememberトークン認証についてもストラテジーとして実装されてます。
2要素認証実装においては、新たなストラテジーとしてOTP認証を実装しました。
認証処理の流れ
まずはDevise認証処理の流れについて解説します。
コントローラにてwarden.authenticate!
のように、Warden::Proxy#authenticate!
を呼ぶことで認証処理が実行されます。deviseで用意されている認証用のアクションであるsessions_controller#create
でも呼ばれていますね。
Warden::Proxy#authenticate!
は次のような順番で処理を実行します。
1. 実行する認証ストラテジー名をシンボル配列で取得します。
2.それぞれのストラテジー名からストラテジークラスを取得し、取得したクラスに対してauthenticate!
※メソッドを呼び出します
※Warden::Proxy#authenticate!
ではなく、ストラテジークラスのauthenticate!
メソッドです。
strategies.each do |name|
strategy = _fetch_strategy(name)
strategy.authenticate!
end
3. ストラテジーの認証が成功すると、対象のユーザーをsessionに格納し、ユーザーオブジェクトを返します。
4. ストラテジーの認証が成功しなければ、sessionの格納はされず、ユーザーも返しません。
ここで実行されるストラテジーは、あらじめdefault_strategies
として設定されたものです。Deviseの場合は、パスワード認証用のdatabase_authenticatable
ストラテジーがdefault_strategies
として、設定されています。加えてRememberableを有効にしている場合は、Rememberトークン認証用のrememberable
ストラテジーもdefault_strategies
に設定されます。
puts strategies # => [:rememberable, :database_authenticatable]
特定のストラテジーを呼び出したい場合は、引数にストラテジー名を渡します。
warden.authenticate!(:custom_strategy, :custom_strategy2, ...)
カスタムストラテジーの実装
カスタムストラテジーを実装はauthenticate!
メソッドを定義する必要があります。定義したauthenticate!
メソッドの中に認証ロジックを実装します。また先程の説明では省略しましたが、authenticate!
を実行するか判定を行うためのvalid?
メソッドを実装する必要があります。
strategies.each do |name| # ... next unless strategy.valid? strategy.authenticate! end
valid?
メソッドをうまく記述することで、「特定の条件だけ、ある認証ストラテジーを無効化する」といった実装ができるようになります。
さて、以上よりカスタムストラテジーは次のような実装となります。
module Devise module Strategies class OtpAuthenticatable < Authenticatable def valid? # ... end def authenticate! resource = TwoFactorAuthentication.get_user(params[:email]) if validate(resource) { OtpAuthentication.valid?(user, params[:otp]) } remember_me(resource) success!(resource) else fail!(:invalid_otp) end end end end end
OTP認証の実装例です。 実際にはパスワードとセットで認証しなければいけないので、もう少し複雑な実装ですが、今回は省略しています。
以下、補足です。
認証ロジック自体はvalidate
メソッドに渡しているブロック内に記述します。validate
はDeviseで用意されたメソッドで、すべての認証ストラテジーにおいて共通の処理が実行されます。例えば、DeviseのLockableモジュール(※)を有効化した場合の、ログイン試行回数のカウントやリセットなどの処理は、validate
に実装されています。
※ログイン試行回数が一定数を超えると該当ユーザーの認証をロックする機能
success!
とfail!
は認証結果をインスタンス変数に格納するメソッドで、認証ストラテジーの呼び出し元で結果を確認するために呼び出すメソッドです。fail!
の引数にはエラー種別を渡します。エラー種別に基づいて、エラーメッセージ等の制御を行います。
次にカスタムストラテジーを呼び出す処理の2パターンについて、それぞれ補足します。
カスタムストラテジーの呼び出し
default_strategiesとして呼び出す
default_strategies
を追加するパターンです。設定はconfig/initializer配下のdevise.rbに書きます。
config.warden do |manager| manager.strategies.add(:custom_database_authenticatable, Devise::Strategies::CustomDatabaseAuthenticatable) # deviseのパスワード認証strategyを上書き manager.default_strategies(scope: :user).delete :database_authenticatable manager.default_strategies(scope: :user).push :custom_database_authenticatable end
strategies.add
でストラテジー名とクラスの関係をマッピングをします。default_strategies
を追加する場合はmanager.default_strategies(scope: :user).push
で追加します。default_strategies
のリストは配列で保持されており、manager.default_strategies(scope: :user)
はdefault_strategies
の配列の参照を返すため、shift
やdelete
などによる変更も可能です。
ストラテジー名を指定して呼び出す
warden.authenticate!(:custom_strategy)
これだけで呼び出すことができますが、default_startegies
と異なる情報を用いた認証を行う場合、呼び出し元のコントローラのどこかでdefault_strategies
が呼び出しされていないか気をつけてください。
具体的には、次のような処理が想定されます。
before_action: :authenticate_user!
をしている。- 同じく
before_action
で、current_user
の返り値を元に判定処理を行っている。
1つ目のケース、authenticate_user!
メソッド内ではWarden::Proxy#authenticate!
が呼ばれます。この時、Deviseの標準だとdefault_strategies
としてパスワード認証が実行されます。しかし、カスタムストラテジー用の認証情報しかないために認証が失敗します。authenticate!
メソッドは認証が失敗するとカスタム認証が実行される前に、ログイン画面にリダイレクトされてしまいます。skip_before_action
や、デフォルトストラテジーのvalid?
メソッドを上書きして実行を制御するなど、対策が必要です。
2つ目のケース、current_user
においては内部でWarden::Proxy#authenticate
が呼ばれます。Warden::Proxy#authenticate!
と異なりリダイレクトこそ発生しませんが、返り値としてnilが返ることを念頭に実装する必要があります。
なお、認証の成功以降は、Wardenで認証ユーザーが保持されるため、デフォルトストラテジー認証周りのケアは特に必要ありません。また、普通のチーム開発だと上記のようなコードは割と気軽に書かれると思いますので、テストも書いた方が良いでしょう。
以上、カスタムストラテジーを定義し、呼び出すための実装を紹介しました。
その他 Tips
ここからは2要素認証の開発において、ストラテジーの追加以外に変更を加えたDeviseの機能について、Tipsをご紹介します。
FailureApp(エラーハンドリング用のクラス)
FailureAppクラスはエラー時のHTTPレスポンスを生成するクラスです。
カスタムFailureAppクラスを作成して、respond
メソッドの返り値を変更することで、エラー時の表示画面やリダイレクト先を変更できます。また、カスタムFailureAppクラスを使用する場合はdevise.rbに設定が必要です。
レスポンスの形式によって、redirectしたりやjsonレスポンスを返すなどの挙動をします。
respondの分岐 | レスポンス内容 | 相当するコントローラ上の処理の例 |
---|---|---|
http_auth | jsonやxml形式 | render :jsonなど |
recall | htmlファイル | render :newなど |
redirect | 302レスポンスする | redirect_to xxx_path |
recall
はwarden.authenticate!(recall: "CustomAuthController#new")
のように引数を渡すことでrender :new
のような動きを実装可能です。エラー種別による分岐をする場合は、warden_message
メソッドで、認証ストラテジーでfail!
したときに引数として渡した値を取り出して利用することができます。
また、FailureAppは、throw(:warden)
をcatch
して呼び出されます。Warden::Proxy#authenticate!
でも認証が失敗して、user変数がnilのときにthrow(:warden)
しています。
def authenticate!(*args) user, opts = _perform_authentication(*args) throw(:warden, opts) unless user user end
hook
Deviseではlib/devise/hooks配下に認証後・ユーザーセット後・ログイン後など、のコールバック処理が定義されています。こちらもstrategyと同様にカスタムhookを作成することができます。
config/initializer/devise.rbにて、Warden::Manager
クラスのコールバックメソッドを呼び出し、ブロック内に実行したい処理を書きます。
Warden::Manager.after_set_user do |user, warden, options| # hook処理 end
要件に合うコールバックメソッドをお探しの場合はこちらを参照ください。
model
lib/devise/models配下にあるファイル群で、modelsと書いていますがユーザーなど認証アカウントクラスでinclude
されるモジュールです。
上書きしたい場合はユーザークラスに同名のメソッドでオーバーライドする形が良いでしょう。deviseのメソッドのうちいくつかは、オーバーライドしてカスタマイズされる前提で処理がかかれています。ちょっとした追加実装であれば、わざわざ新しいストラテジーを定義するまでもなく、実装できることも多いです。
参考
Deviseはgithubのwikiが充実しているので、参考にご覧ください。
メソッドをオーバーライドする以外にも、devise.rbで設定変更できるもの、コントローラでrequest.env['hoge']
に格納された設定値を書き換えられるもの、等があります。
まとめ
以上が、Deviseの実装の解説と、実装に則った新しい認証方法の追加でした。その他Tipsについてもご参考いただけると幸いです。
今回の実装は、お客様の大切な情報をお守りするため責任重大な機能ということで私自身、非常にやりがいを感じながら設計からテスト、リリースまで進めることができました。 弊サービスのTUNAGでは、お客様である企業の成長にエンゲージメントという側面から貢献し、企業様の社員の皆様がより充実した会社生活を送るために、まだまだ開発すべきことがたくさんあります。 エンジニアとして、やりがいのある仕事がしたい!わくわくする仕事をしたい!という方がいらっしゃいましたらぜひ、弊社採用窓口までご連絡ください。
TUNAGの開発体制、使用技術については下記ブログ記事を参照ください。
また株式会社スタメンでは、オンラインサロン用プラットホームFANTSというサービスも開発・運営しております。 ご関心がある方はぜひ下記のリンクをご覧ください。