読者です 読者をやめる 読者になる 読者になる

deviseの独自ストラテジーの作り方

久保田です。

最近、deviseのストラテジー(プラグイン的なもの)を社内の認証システム用に作りました。
その時、古い情報が多く、半泣きになりながらいろいろ模索して作ったので、
備忘録がてら、同じく困る人がいなくなるように記事にします。

devise

Railsで認証つきのアプリケーションを作成したことのある人なら必ず使ったことのあるgemだと思います。
かなり充実したライブラリでデフォルトでよくあるパターンのストラテジーは入っているので、基本的にはそのまま使うことができます。

ストラテジー

deviseではストラテジーという認証のルールを設定することで、必要な機能だけを享受することができます。
例えば、db認証をやりたい時は、

class User < ApplicationRecord
  devise :database_authenticatable

  # ...
end

ロック機能をつけたい時は、

class User < ApplicationRecord
  devise :database_authenticatable, :lockable

  # ...
end

のような感じで宣言します。

今回は、このストラテジーをオリジナルで作ったから作り方を共有します!という話です。

完成イメージ

完成イメージは以下のような感じです。

class User < ApplicationRecord
  devise :foo_authenticatable

  # ...
end

foo_authenticatableというストラテジーを作成します。
これだけで、いつものようにdeviseを使うようにしてオリジナルの認証を通します。

フォルダ構成

最低限必要な構成は以下の通りです。
(lib以下に作りました。)

lib
└── devise
    └── models
        └── foo_authenticatable.rb
    └── strategies
        └── foo_authenticatable.rb

ストラテジーを作るためには、
- modelがincludeするファイル
- 実際に認証を行うstrategyファイル

が必要です。

動作イメージ

動作イメージは、以下のような流れになります。

sessions_controller#create  
↓  
strategies/foo_authenticatable#authenticate!(成功か失敗かを判断する)  
↓  
models/foo_authenticatable.独自のロジック(必要に応じて)  

strategy

まずはstrategyファイルからです。
このファイルは、実際に認証を通していいか、ダメかを判定するロジックを書きます。
以下のような構成になります。

  • strategies/foo_authenticatable.rb
require 'devise/strategies/authenticatable'

module Devise
  module Strategies
    class FooAuthenticatable < Authenticatable
      # optional
      def valid?
        # 事前に行いたいバリデーション。
        # パラメータなどの確認を行う。
        # ex: params['username'] || params['password']
      end
      
      # required
      def authenticate!
        # modelで定義されたメソッドで認証ロジックを通す。modelに書いた方がスッキリするため。
        # mapping.toでストラテジーを宣言したモデルが取得できる。
        res = mapping.to.authenticate(params[scope])
        if res
          success!(res) # 認証成功。
        else
          fail!(:invalid) # 認証失敗。
        end
      end
    end
  end
end

Warden::Strategies.add(:foo_authenticatable, Devise::Strategies::FooAuthenticatable) # ストラテジーの登録

定義されているメソッドは2つ、valid?authenticate!です。
valid?はオプショナルなメソッドなので、あってもなくても大丈夫です。あった場合はauthenticate!の前に動き、
結果がtrueの場合だけauthenticate!を実行します。

authenticate!は必須のメソッドです。
authenticate!の中でsuccess!もしくはfail!を呼び出すことで認証の判定が可能になります。
他のメソッドはこちらです。

これだけで、認証の判定は完成です。
次にmodelに任せた認証ロジックの部分です。

model

次はmodelにロジックを作ります。
modelは最初なくても大丈夫かなーと思っていましたが、ないとないよーってエラーを出しているようでしたので、どうやらいるようです。
そしてこのmodel、自動的にこのストラテジーをdevise :foo_authenticatableのように宣言したモデルにincludeされるようです。
なので、このmodelに実際のロジックを書いていきます。

  • models/foo_authenticatable.rb
require Rails.root.join('lib/devise/strategies/foo_authenticatable')

module Devise
  module Models
    module FooAuthenticatable
      extend ActiveSupport::Concern
            
      module ClassMethods
        def authenticate(attributes)
          # 認証ロジック
          # 仮にデータベースのidで認証
          res = find_by(id: attributes[:id])
          if res.present?
            res
          else
            nil
          end
        end
        
      end
    end
  end
end

最低限はこんな感じです。
extend ActiveSupport::Concernをし、module ClassMethodsを使うと簡単にクラスメソッドが定義できるので、綺麗ですね。
認証が成功したら、インスタンスを返してあげます。後ほどそれがstrategyのsuccess!に渡され、認証ユーザーとなります。

もしaccessorなどを足したい時はincludedを使ってあげましょう。

initializer

最後にinitializer/devise.rbに独自のauthenticationを登録するための処理を書きます。

  • config/initializer/devise.rb
Devise.setup do |config|
  config.warden do |manager|
    manager.default_strategies(scope: :user).unshift :foo_authenticatable
  end

  # ...
end


Devise.add_module(:foo_authenticatable, {
  strategy: true,
  controller: :sessions,
  model: 'devise/models/foo_authenticatable',
  route: :session,
})

これで完了です。
もし独自のストラテジーを作りたくなったらやってみてください。