# gem sorceryを読む会メモ よくわかんないけどなんとなくで使えてるsorceryの中身を読んでみよう企画です。 ## やる事 gem sorceryの中身を追いかけてみようって感じです。 ## Loginの仕組みの確認 session関係のメソッドってどんなんだっけ、自分で書いたらどう書くんだっけの確認です。コードはRUNTEQの補講課題(多分)Exam_RailsBasic_01から引っ張ってきてます。嘘ですloginのところちょっと変えました * login(email, password, remember_me = false) ```ruby def log_in(email, password) user = User.find_by(email: email) session[:user_id] = user.id if user.authenticated(password) end ``` おなじみのログインメソッドsorceryでは引数にemailとpasswordを入れるとログインできるやつ。 * current_user ```ruby def current_user if session[:user_id] @current_user ||= User.find_by(id: session[:user_id]) end end ``` ログインしてるユーザーをcurrent_userに入れて使えるようにするやつ。 * logout ```ruby def log_out session.delete(:user_id) @current_user = nil end ``` sessionとcurrent_userを消してログアウト。 * logged_in? ```ruby def logged_in? !current_user.nil? end ``` current_userの状態を確認してログイン状態を確認する。 * require_login application_controllerによく書くやつ。ログインしていないとダメって言われる * auto_login(user) ユーザー登録後にすぐログインしたい時とかゲストログインさせたいときとかに使う * redirect_back_or_to ログイン前のところにリダイレクトさせたい時とかに使うやつ。 ## sorceryで使われるメソッドの定義場所を探そう メソッドがどう定義されているか探してみよう sorceryを使っているアプリにpry-byebugを入れる `gem 'pry-byebug'` `bundle install` sorceryを使っているアプリのみたいメソッドの前の行に `binding.pry`を書いて、実際にアプリを動かして読み解いていきます。 ### pry-byebugの使い方 pry-byebugで使えるコマンド - next 次の行を実行 - step 次の行かメソッド内に入る - continue プログラムの実行をcontinueしてpryを終了 - finish 現在のフレームが終わるまで実行 ## 実際にメソッドを見ていく * login アプリ画面 [image:31AA5521-42DF-4755-BA2F-2CA019055752-4169-000043903B6F374A/C3F36484-F9E4-49B6-96B6-461D2FF4921F.png] Loginボタンを押すと処理が止まってtarminalにこんな感じで表示されるので、pryのstep, nextを使って中身を追いかけていきます。 ```ruby From: /Users/nakaikensuke/environment/output-matching/app/controllers/user_sessions_controller.rb:6 UserSessionsController#create: 4: def create 5: binding.pry => 6: @user = login(params[:email], params[:password]) 7: 8: if @user 9: redirect_to root_path 10: else 11: flash.now[:alert] = 'Login failed' 12: render action: 'new' 13: end 14: end ``` ステップ実行 ```ruby [1] pry(#<UserSessionsController>)> step From: /Users/nakaikensuke/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/actionpack-6.0.3.3/lib/action_controller/metal/strong_parameters.rb:1099 ActionController::StrongParameters#params: 1098: def params => 1099: @_params ||= Parameters.new(request.parameters) 1100: end ``` この状況で`params`と打つとparamsの中に何が入っているか見れます。 ```ruby [1] pry(#<UserSessionsController>)> params => <ActionController::Parameters {"authenticity_token"=>"9ZyX/8lPgaPA7GELXnj/fUrmrtf+HNyfanRLgvu3S9XnC4lpjtD/dYJHIuXAUr1PIWVuYDaB+K6XU6obfNdvTQ==", "email"=>"test@example.com", "password"=>"pass", "commit"=>"Login", "controller"=>"user_sessions", "action"=>"create"} permitted: false> ``` アプリ画面で打ち込んだメールアドレスとパスワードとかの情報が入っています。 ## いよいよloginの中身を見ていく 何回か`next `を実行するとsorceryで定義されているlogin(*credentials)の中に入れたみたいです。 ```ruby [3] pry(#<ActionController::Parameters>)> next From: /Users/nakaikensuke/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/sorcery-0.15.0/lib/sorcery/controller.rb:38 Sorcery::Controller::InstanceMethods#login: 37: def login(*credentials) => 38: @current_user = nil 39: 40: user_class.authenticate(*credentials) do |user, failure_reason| 41: if failure_reason 42: after_failed_login!(credentials) 43: 44: yield(user, failure_reason) if block_given? 45: 46: # FIXME: Does using `break` or `return nil` change functionality? 47: # rubocop:disable Lint/NonLocalExitFromIterator 48: return 49: # rubocop:enable Lint/NonLocalExitFromIterator 50: end 51: 52: old_session = session.dup.to_hash 53: reset_sorcery_session 54: old_session.each_pair do |k, v| 55: session[k.to_sym] = v 56: end 57: form_authenticity_token 58: 59: auto_login(user, credentials[2]) 60: after_login!(user, credentials) 61: 62: block_given? ? yield(current_user, nil) : current_user 63: end 64: end ``` 今回はこれをstep実行で見ていきます。 その前に気になるのが `def login(*credentials)` とかいう引数に謎の記号がついてるやつです。 ### 可変長引数 引数の前に`*`をつけると複数の値を渡せるようになります。 これは可変長引数という名前のようです。 sorceryでログインする際にはemailとpassword、あとremember_meのオプションを渡せます。 なので`login(email, password)`こんな感じに引数を渡すと中身は `[email, password]` のように配列になり、 ```ruby puts credentials[0] puts credentials[1] ``` こんな感じで扱えるようになります(後で出てきます)。 ### user_class 何回かstep実行すると`user_class`の中に入りました。 `user_class.authenticate(*credentials) do |user, failure_reason|` ```ruby [5] pry(#<UserSessionsController>)> step From: /Users/nakaikensuke/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/sorcery-0.15.0/lib/sorcery/controller.rb:166 Sorcery::Controller::InstanceMethods#user_class: 165: def user_class => 166: @user_class ||= Config.user_class.to_s.constantize 167: rescue NameError 168: raise ArgumentError, 'You have incorrectly defined user_class or have forgotten to define it in intitializer file (config.user_class = \'User\').' 169: end ``` ユーザークラスの中にはClassが入っていました。 ```ruby [7] pry(#<UserSessionsController>)> user_class => User(id: integer, name: string, email: string, crypted_password: string, salt: string, created_at: datetime, updated_at: datetime) [8] pry(#<UserSessionsController>)> user_class.class => Class ``` ちなみに`Config.user_class`には文字列の`"User"`が入っていました。 ```ruby [9] pry(#<UserSessionsController>)> Config.user_class => "User" ``` これがどこで定義されているかというと `/config/initializers/sorcery.rb`の下の方にある ここです。 ```ruby # This line must come after the 'user config' block. # Define which model authenticates with sorcery. config.user_class = "User" ``` > constantizeメソッドは、レシーバの定数参照表現を解決し、実際のオブジェクトを返します > Rails Guide リンク [5.11.12 constantize](https://railsguides.jp/active_support_core_extensions.html?version=6.0#constantize) 正直なんのこっちゃって感じですが、文字列`"User"`が入っている `user_class`にconstantizeメソッドを使うことで文字列を定数化して、該当するものがないか探してきてくれるみたいなイメージで認識しました。 つまるところuser_classにはUserクラスが入っている感じなのかな(まんまやんけ)。 ### authenticate(*credentials) ```ruby def authenticate(*credentials, &block) raise ArgumentError, 'at least 2 arguments required' if credentials.size < 2 if credentials[0].blank? return authentication_response(return_value: false, failure: :invalid_login, &block) end if @sorcery_config.downcase_username_before_authenticating credentials[0].downcase! end user = sorcery_adapter.find_by_credentials(credentials) unless user return authentication_response(failure: :invalid_login, &block) end set_encryption_attributes if user.respond_to?(:active_for_authentication?) && !user.active_for_authentication? return authentication_response(user: user, failure: :inactive, &block) end @sorcery_config.before_authenticate.each do |callback| success, reason = user.send(callback) unless success return authentication_response(user: user, failure: reason, &block) end end unless user.valid_password?(credentials[1]) return authentication_response(user: user, failure: :invalid_password, &block) end authentication_response(user: user, return_value: user, &block) end ``` stepをおこなってauthenticateメソッドの中に入りました。 `user_class.authenticate(*credentials) do |user, failure_reason|` ```ruby [10] pry(#<UserSessionsController>)> step From: /Users/nakaikensuke/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/sorcery-0.15.0/lib/sorcery/model.rb:87 Sorcery::Model::ClassMethods#authenticate: 82: # The default authentication method. 83: # Takes a username and password, 84: # Finds the user by the username and compares the user's password to the one supplied to the method. 85: # returns the user if success, nil otherwise. 86: def authenticate(*credentials, &block) => 87: raise ArgumentError, 'at least 2 arguments required' if credentials.size < 2 88: 89: if credentials[0].blank? 90: return authentication_response(return_value: false, failure: :invalid_login, &block) 91: end 92: ``` まず1個目、 ```ruby raise ArgumentError, 'at least 2 arguments required' if credentials.size < 2 ``` 引数が2個以下だったらArgumentErrorを起こします。 ArgumentErrorってなんだっけ > 引数の数があっていないときや、数は合っていて、期待される振る舞いを持ってはいるが、期待される値ではないときに発生します。 > [class ArgumentError (Ruby 2.7.0 リファレンスマニュアル)](https://docs.ruby-lang.org/ja/latest/class/ArgumentError.html) まれによく見るこんなやつ ```ruby wrong number of arguments (given 0, expected 1..2) (ArgumentError) ``` 2個目 ```ruby if credentials[0].blank? return authentication_response(return_value: false, failure: :invalid_login, &block) end ``` 引数で渡したemailがblankのときauthentication_responseに, `return_value: false, failure: :invalid_login, &block` の引数を渡す。 authentication_responseの中身 ```ruby def authentication_response(options = {}) yield(options[:user], options[:failure]) if block_given? options[:return_value] end ``` `options = {}` また見慣れない書き方が出てきました。 これは引数に渡されたoptionsを1つのハッシュにまとめてくれるみたいです。 yieldって結局なにしてるの > 自分で定義したブロック付きメソッドでブロックを呼び出すときに使います。 yield に渡された値はブロック記法において | と | の間にはさまれた変数(ブロックパラメータ)に代入されます。 > [メソッド呼び出し(super・ブロック付き・yield) (Ruby 2.7.0 リファレンスマニュアル)](https://docs.ruby-lang.org/ja/latest/doc/spec=2fcall.html) 3個目 ```ruby if @sorcery_config.downcase_username_before_authenticating credentials[0].downcase! end ``` これはsorceryの設定でemailを全て半角に変換する時に動くみたいです。 4個目 ```ruby user = sorcery_adapter.find_by_credentials(credentials) unless user return authentication_response(failure: :invalid_login, &block) end ``` userがないとき `authentication_responce(failure: :invalid_login, &block)` をreturnする `find_by_credentials`はユーザーを定義してくれる。 ```ruby def find_by_credentials(credentials) relation = nil @klass.sorcery_config.username_attribute_names.each do |attribute| if @klass.sorcery_config.downcase_username_before_authenticating condition = @klass.arel_table[attribute].lower.eq(@klass.arel_table.lower(credentials[0])) else condition = @klass.arel_table[attribute].eq(credentials[0]) end relation = if relation.nil? condition else relation.or(condition) end end @klass.where(relation).first end ``` @klassってなんぞclassじゃないの? > classは [Ruby](http://d.hatena.ne.jp/keyword/Ruby) の [予約語](http://d.hatena.ne.jp/keyword/%CD%BD%CC%F3%B8%EC) (class Foo ~~~ end)なので、引数の名前として使えません。 > そこでklassという識別子をつくり、実質クラスを表すものであるかのように割り当てます。 > 無理にklassという変数名で定義する必要はないけれど、 [Rubyist](http://d.hatena.ne.jp/keyword/Rubyist) の文脈としてはklassという表現を好んでいます。 > [RubyのKlassて何 - ミライトアルマチ通信](https://keisei1092.hatenablog.com/entry/2016/08/16/112242) 5個目 `set_encryption_attributes` ```ruby def set_encryption_attributes @sorcery_config.encryption_provider.stretches = @sorcery_config.stretches if @sorcery_config.encryption_provider.respond_to?(:stretches) && @sorcery_config.stretches @sorcery_config.encryption_provider.stretches = @sorcery_config.salt_join_token if @sorcery_config.encryption_provider.respond_to?(:join_token) && @sorcery_config.salt_join_token @sorcery_config.encryption_provider.pepper = @sorcery_config.pepper if @sorcery_config.encryption_provider.respond_to?(:pepper) && @sorcery_config.pepper end ``` `@sorcery_config.encryption_provider.stretches`と `@sorcery_config.encryption_provider.stretches`と `@sorcery_config.encryption_provider.pepper`を定義している。 6個目 ```ruby if user.respond_to?(:active_for_authentication?) && !user.active_for_authentication? return authentication_response(user: user, failure: :inactive, &block) end ``` 7個目 ```ruby @sorcery_config.before_authenticate.each do |callback| success, reason = user.send(callback) unless success return authentication_response(user: user, failure: reason, &block) end end ``` respond_to?とは? > オブジェクトがメソッド name を持つとき真を返します。 > オブジェクトが メソッド name を持つというのは、オブジェクトが メソッド name に応答できることをいいます。 > [Object#respond_to? (Ruby 2.7.0 リファレンスマニュアル)](https://docs.ruby-lang.org/ja/latest/method/Object/i/respond_to=3f.html)