devise_token_authで、Email以外でも認証を可能にする方法


deviseのデフォルトでの認証はメールアドレスのみです。

今回は、「メールアドレス」または「電話番号」、どちらでも認証を行えるよう実装したいと思います。

Userモデルに認証のためのキーを追加する


class User < ActiveRecord::Base
  extend Devise::Models
  devise :database_authenticatable, :registerable,
         :rememberable, :trackable, :validatable,
         authentication_keys: [:login]
  include DeviseTokenAuth::Concerns::User

  # ...

end

authentication_keys: [:login]にて、 認証のためのキーを:emailから、:loginに変更します。

クライアント側からは、loginキーに「電話番号」または「メールアドレス」を入れて送ります。

Sessionsコントローラーを変更する

DeviseTokenAuth::SessionsControllerのcreateアクションをオーバーライドしてます。


class Api::V1::Auth::SessionsController < DeviseTokenAuth::SessionsController

  def create
    field = (resource_params.keys.map(&:to_sym) & resource_class.authentication_keys).first
    @resource = nil

    if field
      q_value = get_case_insensitive_field_from_resource_params(field)

      @resource = find_resource(:email, q_value) if q_value.match?(/@/)
      @resource = find_resource(:phone_number, q_value) if q_value.match?(/\A\d{10}$|^\d{11}\z/)
    end

    if @resource && valid_params?(field, q_value) && (!@resource.respond_to?(:active_for_authentication?) || @resource.active_for_authentication?)
      valid_password = @resource.valid_password?(resource_params[:password])
      if (@resource.respond_to?(:valid_for_authentication?) && !@resource.valid_for_authentication? { valid_password }) || !valid_password
        return render_create_error_bad_credentials
      end
      @token = @resource.create_token
      @resource.save

      sign_in(:user, @resource, store: false, bypass: false)

      yield @resource if block_given?

      render_create_success
    elsif @resource && !(!@resource.respond_to?(:active_for_authentication?) || @resource.active_for_authentication?)
      if @resource.respond_to?(:locked_at) && @resource.locked_at
        render_create_error_account_locked
      else
        render_create_error_not_confirmed
      end
    else
      render_create_error_bad_credentials
    end
  end

  #...

  private

  def resource_params
    params.permit(:password, :login)
  end

end

理想はsuperで呼び出して処理したいんですが、今回は無理でした。

なのでcreateメソッドを作り、そこに親のメソッドの処理を貼り付けて、必要な処理を追加という形にしてます。

主要なところだけ解説します。

ストロングパラメータでの受け取り


def resource_params
  params.permit(:password, :login)
end

見ての通りです。deviseのストロングパラメータをオーバーライドしましょう。

積集合を取る


field = (resource_params.keys.map(&:to_sym) & resource_class.authentication_keys).first

resource_params.keys.map(&:to_sym)は、送られてきたパラメータのkeyをチェックし、配列にします。たとえば、値は[:password, :login]とかになります。

resource_class.authentication_keysは、先程設定した認証のためのkeyで、つまり[:login]です。

&でこの2つの積集合を取り、([:login]となります。)firstメソッドで、その中身を取っています。

つまり field => :loginです。

fieldがあればあとはDBまで潜りに行くだけ


    if field
      q_value = get_case_insensitive_field_from_resource_params(field)

      @resource = find_resource(:email, q_value) if q_value.match?(/@/)
      @resource = find_resource(:phone_number, q_value) if q_value.match?(/\A\d{10}$|^\d{11}\z/)
    end

get_case_insensitive_field_from_resource_params(field)は、resource_params[login]にスペースがあったら削除してくれるメソッドです。:emailキーの場合は、downcase!してくれますが、今回は:loginキーにしているので関係ないです。

あとはfind_resourceメソッドで入力された情報に合うユーザーがいるか探しにいきましょう。(クエリの発行)

ヒットしたら@resoucerにそのユーザーを返します。

その後の処理は、もともとの処理と変更ありませんので、解説はしません。

これで、「メールアドレス」または「電話番号」での認証について、成功です。

<補足>

現状だと、メールアドレスが認証のためのユニークなキー(uid)となっておりNULLを受け付けません。

電話番号をuidにしたい場合、以下のような対応策があります。

user.rb

class User < ActiveRecord::Base

  # ...

  before_validation :set_uid_from_phone_number

  def set_uid_from_phone_number
    self.uid = phone_number
  end

  # ...

end
sessions_controller.rb

  #オーバーライドで、providerをデフォルト設定である"email"から変更
  def provider
    "phone_number"
  end

参考

(公式ドキュメント)
https://github.com/plataformatec/devise/wiki/How-To:-Allow-users-to-sign-in-using-their-username-or-email-address