Meguro.rbで「貴方はOmniAuth::AuthHashを知っていますか?」というタイトルでLTしました。あるいは、HashieとActiveModel::ForbiddenAttributesErrorについて
Meguro.rb#18 in ドリコム社さんで「貴方はOmniAuth::AuthHashを知っていますか?」というタイトルでLTをしました。
OmniAuth::AuthHashについて
OmniAuthは皆さん知っていると思います。
語弊を恐れずに言えば、RubyでOAuth認証の仕組みを簡単に提供するライブラリです。
その中でgemspecを見てみると、hashieというライブラリを使っています。
hashieというライブラリはHashClassをより使いやすくするライブラリです。
Hashieについて
module OmniAuth
class AuthHash < OmniAuth::KeyStore
def self.subkey_class
Hashie::Mash
end
......
......
......
end
OmniAuth::AuthHashというclassで利用されています。AuthHashというのは、スライドで紹介しましたこのページに記載しています。
https://speakerdeck.com/teitei/gui-fang-haomniauth-authhashwozhi-tuteimasuka?slide=9
CallbackPhaseというはOAuthProviderからAuthorized(認可がおりている)場合のパラメータです。Railsで言うと下記のようなコードです。
class SessionController < ApplicationController
def create
# do something
puts auth_hash
end
protected
def auth_hash
request.env['omniauth.auth']
end
end
ControllerでCallbackParameterを利用して、データを加工すると思います。
parameterの中身も実はGithubに乗っています。
https://github.com/omniauth/omniauth/wiki/Auth-Hash-Schema
その後、よくあるのは下のようにActiveModelとして扱うことではないでしょうか。。
class Github::Schema
include ActiveModel::Model
include ActiveModel::Attributes
validates :provider, :uid, presence: true
attribute :provider, :string
attribute :uid, :string
# do something
end
実際に下記のコードを見てみましょう。
auth_hash = ::OmniAuth::AuthHash.new({ provider: :github, uid: "abcd1"})
=> {"provider"=>:github, "uid"=>"abcd1"}
github = Github.new(auth_hash)
=> ActiveModel::ForbiddenAttributesError: ActiveModel::ForbiddenAttributesError
ActiveModel::ForbiddenAttributesError: ActiveModel::ForbiddenAttributesError
になります。解せぬ。
なぜこのような挙動になってしまうのか。
下記の挙動を見てもらうとわかると思います。
[9] auth_hash.respond_to?(:permitted?)
=> true
[10] auth_hash.permitted?
=> false
そしてActiveModelのソースコードです。
auth_hashというのは前述の通り、実質はHashieというライブラリです。
そこでHashieのコードを見てたいと思います。
Object#respond_to_missing?を継承しています。またコードを読むと、method名にsuffix(?, !など)がついていると強制的にtrueを返しています。なので、前述のを継承しています。
またコードを読むと、method名にsuffix(?, !など)がついていると強制的にtrueを返しています。なので、前述の
auth_hash.respond_to?(:permitted?)
がtrueになってしまいます。
更にObject#method_missingを継承しています。コードを追ってみると、method名のsuffixを取得し、case分にて?の場合は !!self[name] を返しています。コードに落とすと下記のコードになります。
auth_hash = ::OmniAuth::AuthHash.new
=> {}
auth_hash[:permited?]
=> nil
!!auth_hash[:permited?]
=> false
と、permitted?を読んだ際にfalseが帰ってくることがわかると思います。
その上でForbiddenAttributesErrorを呼ぶかどうかの処理を追っていきたいと思います。
module ActiveModel
class ForbiddenAttributesError < StandardError
end
module ForbiddenAttributesProtection # :nodoc:
private
def sanitize_for_mass_assignment(attributes)
if attributes.respond_to?(:permitted?)
raise ActiveModel::ForbiddenAttributesError if !attributes.permitted?
attributes.to_h
else
attributes
end
end
alias :sanitize_forbidden_attributes :sanitize_for_mass_assignment
end
end
こうしてみると、上記2つが原因でForbiddenAttributesErrorになる理由がわかると思います。
なぜHashieを使っているのか?
追ってみましたが、これがあるからHashieを使っているという明確な理由が自分には探すことができませんでした。
ここからは私見ですが、数多くのOAuthProviderに対応するためにはただのHashClassでは機能不足で、Hashを改良しているHashieにしたのでは無いかと思います。
対策
1. auth_hashを利用してActiveModel・ActiveRecordを作らない。
2. Hashieから再帰関数を使ってただのHashClassにする。
他にもあると思いますが、要はHashieの問題なので、callback phaseにて帰ってくるパラメータをよしなに加工をすると良いと思います。