【devise_token_auth】sessions_controllerの解説②【sign_in】

rails

本記事ではdevise_token_authのsign_inの処理(ログイン成功時)について解説していきます。

前回の続きから始めていきます。前回では@resourceがsign_in時に入力したemailと合致したデータをデータベースから取ってきているというところまで解説しました。

今回はその続きで、取ってきたデータを登録するまでを解説していきます。

session_controllerのソースコードについて

devise_token_authのsessions_controllerのソースコードはこちらになります。

sign_inの処理を一つずつ解説していく

createアクションの処理の全体像はこちらになります。その中でも、下記の部分を取得していきます。

def create
  if field = (resource_params.keys.map(&:to_sym) & resource_class.authentication_keys).first
    q_value = get_case_insensitive_field_from_resource_params(field)

    @resource = find_resource(field, q_value)
  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

    create_and_assign_token

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

    yield @resource if block_given?

    render_create_success
  elsif @resource && !Devise.paranoid && !(!@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
    hash_password_in_paranoid_mode
    render_create_error_bad_credentials
  end
end
############################# ここまで #############################

それでは一つずつ見ていきましょう!

最初のif文について

まずは最初のif文からです

# (1) 20行目
if @resource && valid_params?(field, q_value) 
  && (!@resource.respond_to?(:active_for_authentication?) || @resource.active_for_authentication?)

合計3つの条件があります。

1つ目は、@resourceが存在するか

2つ目のvalid_params?(field, q_value)は解説①でfieldがsign_in時に認証するカラム(何も設定していなければemail)で、q_valueはsign_inで入力した値ということがわかっています。valid_paramsについてみてみると、

# (1-1) 71行目
def valid_params?(key, val)
  resource_params[:password] && key && val
end

この様に、paramsにパスワードがあることとsign_inの認証カラムがあるかどうかを判断しています。

3つ目は、2つのどちらかを満たせばいいわけですが、まず一つ目は、

!@resource.respond_to?(:active_for_authentication?)

これは、@resourceにactive_for_authenticationメソッドがない場合にtrueとなります。

@resource.active_for_authentication?

上記は、active_for_authenticationメソッドがある場合、trueとなっていれば認証できるが、falseの場合認証エラーを返す処理になっています。

3つ目の処理は、認証エラーがないユーザーかを確認している処理になります。

valid_passwordと2つ目のif文について

valid_password

まずはvalid_passwordについて。

# (2) 21行目
valid_password = @resource.valid_password?(resource_params[:password])

valid_password?メソッドを確認する必要があり、これはdeviseのmodelにあります(2-1)

# (2-1) 71行目
def valid_password?(password)
  Devise::Encryptor.compare(self.class, encrypted_password, password)
end

paramsは、self.classは、Devise::Models::DatabaseAuthenticatableを表していて、encrypted_passwordは、データベースに保存されているパスワードをハッシュ化した値をセットしています。

今度はDevise::Encryptor.compareメソッド(2-1-1)を確認します。

# (2-1-1)
require 'bcrypt'
module Devise
  module Encryptor
    def self.compare(klass, hashed_password, password) # 14行目
      return false if hashed_password.blank?
      bcrypt   = ::BCrypt::Password.new(hashed_password)
      if klass.pepper.present?
        password = "#{password}#{klass.pepper}"
      end
      password = ::BCrypt::Engine.hash_secret(password, bcrypt.salt)
      Devise.secure_compare(password, hashed_password)
    end

まず、ハッシュ化されたパスワードが無ければfalseが返されます。ここの処理では、bcryptライブラリを使用して、ハッシュ化したパスワードとパスワードに違いがないかを確認しています(合っていればtrue)。

if文について

続いて、22行目のif文のところ

# (3)22行目
if (@resource.respond_to?(:valid_for_authentication?) && !@resource.valid_for_authentication? { valid_password }) || !valid_password
  return render_create_error_bad_credentials
end

こちらも大きく分けると2つになっています。前半から見ていきましょう

@resource.respond_to?(:valid_for_authentication?) && !@resource.valid_for_authentication? { valid_password }

こちら2つの条件の&条件になります。ここの処理もactive_for_authentication?とやっていることが近いですね。まずはvalid_for_authenticationメソッドがあるか、ある場合、後半の処理がfalseになるかを確認しています。valid_for_authenticationを見ていきます。

# (3-1) 102行目
def valid_for_authentication?
        return super unless persisted? && lock_strategy_enabled?(:failed_attempts)

        # Unlock the user if the lock is expired, no matter
        # if the user can login or not (wrong password, etc)
        unlock_access! if lock_expired?

        if super && !access_locked?
          true
        else
          increment_failed_attempts
          if attempts_exceeded?
            lock_access! unless access_locked?
          else
            save(validate: false)
          end
          false
        end
      end

こちらは、アカウントロックに関する処理の様です。deviseの:lockableを指定した場合に適用されます(詳しくは下記記事)。今回は適用していないのでfalseになりますね。

このif文がtrueの場合(アカウントロックがかかっている、もしくはvalid_passwordがfalse)エラーが返される処理になります。

create_and_assign_tokenからrender_create_successまで

26行目 create_and_assign_token

create_and_assign_tokenのソースはこちらになります。

# (4-1) 133行目
def create_and_assign_token
      if @resource.respond_to?(:with_lock)
        @resource.with_lock do
          @token = @resource.create_token
          @resource.save!
        end
      else
        @token = @resource.create_token
        @resource.save!
      end
    end

ここでは、tokenを発行しています。with_lockはトランザクション内でリソースをロックする処理ですが、今回は処理の有無しで分岐していて、中身はどちらもtokenを作成する同じ処理になります。create_tokenメソッドを見ると、

# (4-1-1) 92行目
def create_token(client: nil, lifespan: nil, cost: nil, **token_extras)
      token = DeviseTokenAuth::TokenFactory.create(client: client, lifespan: lifespan, cost: cost)

      tokens[token.client] = {
        token:  token.token_hash,
        expiry: token.expiry
      }.merge!(token_extras)

      clean_old_tokens

      token
    end

この様になっており、tokenはDeviseTokenAuth::TokenFactoryで定義されていますね。これも見てみます。

# (4-1-1-1) 62行目  
    def self.create(client: nil, lifespan: nil, cost: nil)
      # obj_client  = client.nil? ? client() : client
      obj_client  = client || client()
      obj_token      = token
      obj_token_hash = token_hash(obj_token, cost)
      obj_expiry     = expiry(lifespan)

      Token.new(obj_client, obj_token, obj_token_hash, obj_expiry)
    end

client、token(token_hash)、expiryを定義して登録しています。それぞれ何を設定していくかみていきましょう(疲れた)。

# (4-1-1-1-1) 30行目
    def self.client
      secure_string
    end
# (4-1-1-1-1-1)
    def self.secure_string      
      SecureRandom.urlsafe_base64
    end

clientがnilのため、clientメソッドをみます。clientを追っていくと、(4-1-1-1-1-1)でランダムな文字列を作成するメソッドに辿り着きます。

続いてtoken、

# (4-1-1-1-2) 38行目
    def self.token
      secure_string
    end

こちらもsecure_stringでランダム文字列を作成していますね!ラスト、expiry!

 # (4-1-1-1-3) 62行目
    def self.expiry(lifespan = nil)
      lifespan ||= DeviseTokenAuth.token_lifespan
      (Time.zone.now + lifespan).to_i
    end

こちらのtoken_lifespanはconfigファイルで設定する箇所があります。

# config.token_lifespan = 2.weeks

初期値は2週間になります。もし、変更したければ、コメントアウトを外して設定することで反映させることができます。

ここの処理では、client、token(token_hash)、expiryを作成し、登録する処理をしている箇所になります。

28行目 sign_inメソッド

# (5) 28行目
sign_in(@resource, scope: :user, store: false, bypass: false)

こちらは、sign_inでrailsの情報をログイン状態にするメソッドです。ちょっと理解できていないところもあるので、今後調べて追記していきます。

30行目 yield @resource if block_given?

blockが渡された場合に、@resourceにblockを付与します。

render_create_success

こちら、成功時のレスポンスです。success trueとmessageが返されます。

# 64行目
devise_token_auth/app/controllers/devise_token_auth/unlocks_controller.rb
   def render_create_success
      render json: {
        success: true,
        message: success_message('unlocks', @email)
      }
    end
# 79行目 app/controllers/devise_token_auth/application_controller.rb
    def success_message(name, email)
      if Devise.paranoid
        I18n.t("devise_token_auth.#{name}.sended_paranoid")
      else
        I18n.t("devise_token_auth.#{name}.sended", email: email)
      end
    end

まとめ

ログイン成功時の処理について解説しました。

tokenなどどうやって作られているかを理解することができました。sign_in処理を実践する際は、こちらの記事を読み返しながら行っていこうと思います。

そして、この記事も随時書き直していって、分かりやすくしていこうと思います!

コメント

タイトルとURLをコピーしました