見出し画像

Meguro.rbで「貴方はOmniAuth::AuthHashを知っていますか?」というタイトルでLTしました。あるいは、HashieとActiveModel::ForbiddenAttributesErrorについて

Meguro.rb#18 in ドリコム社さんで「貴方はOmniAuth::AuthHashを知っていますか?」というタイトルでLTをしました。

OmniAuth::AuthHashについて

OmniAuthは皆さん知っていると思います。

語弊を恐れずに言えば、RubyでOAuth認証の仕組みを簡単に提供するライブラリです。

その中でgemspecを見てみると、hashieというライブラリを使っています。

https://github.com/omniauth/omniauth/blob/8179ba796aae82f857f63b50ae848a3fbe369b4d/omniauth.gemspec#L8

hashieというライブラリはHashClassをより使いやすくするライブラリです。

Hashieについて

module OmniAuth
  class AuthHash < OmniAuth::KeyStore
    def self.subkey_class
      Hashie::Mash
    end
  ......
  ......
  ......
end

https://github.com/omniauth/omniauth/blob/40d8bd35bfb68f8017e6cdc569c33fcb950918e3/lib/omniauth/auth_hash.rb

https://github.com/omniauth/omniauth/blob/40d8bd35bfb68f8017e6cdc569c33fcb950918e3/lib/omniauth/key_store.rb

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のコードを見てたいと思います。

https://github.com/intridea/hashie/blob/4893ca938e243acb07513c3c8b773acf26f72be6/lib/hashie/mash.rb#L251

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にて帰ってくるパラメータをよしなに加工をすると良いと思います。


Software Engineer. https://teitei-tk.com