# 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)