This page looks best with JavaScript enabled
⚠️

【Rails】Sorceryの外部認証でコールバックURLを動的に変える

 ·   ·  ☕ 4 分で読めます
✏️

SorceryのコールバックURLは基本的にconfigファイルで変更するのですが、コントローラから動的に変えたいときはどうすればいいのかわからなかったので、いろいろ試してみました。

力任せなやり方

こんな感じで、@provider.callback_urlにコールバック先のURLを入れる。

1
2
3
4
5
6
7
class OauthsController < ApplicationController
  def oauth
    login_at(auth_params[:provider])
    # 極端なことを言うと、こんなコールバック先にもできる
    @provider.callback_url = 'https://www.google.com/'
  end
end

実際に動的な値を検証

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class OauthsController < ApplicationController
  def oauth
    login_at(auth_params[:provider])
    if params[:hoge] == 'hoge'
      @provider.callback_url = 'http://127.0.0.1:3000/oauth/callback?provider=github&hoge=hoge'
    elsif params[:hofe] == 'foo'
      @provider.callback_url = 'http://127.0.0.1:3000/oauth/callback?provider=github&hoge=foo'
    else
      # 特に指定なし
    end
  end
end
1
2
3
= link_to 'Login with NoParams', auth_at_provider_path(provider: :github)
= link_to 'Login with Hoge', auth_at_provider_path(provider: :github, hoge: 'hoge')
= link_to 'Login with Foo', auth_at_provider_path(provider: :github, hoge: 'foo')

これで試したところ、callbackアクションに渡されるparams[:hoge]

Login with NoParams……前回の値
Login with Hoge……"hoge"
Login with Foo……"foo"

となる。

Sorceryのコード

この記事ではGitHub認証を挙げるが、他の認証でも差異はそれほどなさそう。

Sorceryのexternalを使うことで、下記のモジュールがコントローラにincludeされることになっている。
sorcery/external.rb at 5fae86fcaf836e32d29ff456ce96bcb65dda6922 · Sorcery/sorcery

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
module Sorcery
  module Controller
    module Submodules
      module External
        module InstanceMethods
          protected

          # 一部省略

          def sorcery_login_url(provider_name, args = {})
            @provider = sorcery_get_provider provider_name
            sorcery_fixup_callback_url @provider

            return nil unless @provider.respond_to?(:login_url) && @provider.has_callback?

            @provider.state = args[:state]
            @provider.login_url(params, session)
          end

          # このメソッドをログイン時のコントローラで呼び出している
          def login_at(provider_name, args = {})
            redirect_to sorcery_login_url(provider_name, args)
          end
        end
      end
    end
  end
end

つまり、login_atは、最終的には@provider.login_url(params, session)の返り値にリダイレクトしているだけ。
この@providerSorcery::Providers::Githubのインスタンスである。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[1] pry(#<OauthsController>)> sorcery_get_provider 'github'
=> #<Sorcery::Providers::Github:0x00007fb8cda73978
 @auth_path="/login/oauth/authorize",
 @callback_url="http://127.0.0.1:3000/oauth/callback?provider=github",
 @key="*********",
 @original_callback_url="http://127.0.0.1:3000/oauth/callback?provider=github",
 @scope="user:email",
 @secret="********************",
 @site="https://github.com/",
 @state=nil,
 @token_url="/login/oauth/access_token",
 @user_info_mapping={:display_name=>"name", :email=>"email", :screen_name=>"login"},
 @user_info_path="https://api.github.com/user">

Sorcery::Providers::Githubにlogin_urlが定義されている。これが認証時の遷移先URLになる。
sorcery/github.rb at 5fae86fcaf836e32d29ff456ce96bcb65dda6922 · Sorcery/sorcery

ちなみに、GithubモデルはSorcery::Providers::Baseを継承しており、ここにattributeとしてcallback_urlが定義されている。
sorcery/base.rb at 5fae86fcaf836e32d29ff456ce96bcb65dda6922 · Sorcery/sorcery

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
module Sorcery
  module Providers
    class Github < Base
      include Protocols::Oauth2

      def login_url(_params, _session)
        authorize_url(authorize_url: auth_path)
      end
    end
  end
end

また、Sorcery::Protocols::Oauth2をincludeしているので、以下のメソッドが内包されているのと同義である。
sorcery/oauth2.rb at 6fdc703416b3ff8d05708b05d5a8228ab39032a5 · Sorcery/sorcery

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
module Sorcery
  module Protocols
    module Oauth2
      def authorize_url(options = {})
        client = build_client(options)
        client.auth_code.authorize_url(
          redirect_uri: @callback_url, # ここで、リダイレクトURLとしてmodelに定義されたcallback_urlを渡している
          scope: @scope,
          display: @display,
          state: @state
        )
      end
    end
  end
end

つまり、@providerであるGithubインスタンスのcallback_urlにコールバックURLを代入すればいい。

この値は、デフォルトではconfig/initializers/sorcery.rbで設定した値になる。

1
2
3
4
Rails.application.config.sorcery.configure do |config|
  # 省略
  config.github.callback_url = 'http://127.0.0.1:3000/oauth/callback?provider=github'
end

しかし、この@providerは同一のオブジェクトなので、callback_urlが初期値にならず、以前のcallback_urlを保持してしまう。そのため、@provider.callback_url =を設定しない場合は、前回の値のままになる。

なので、@provider.callback_url =で動的にコールバックURLを変更する場合は、どのパターンであろうと設定するようにする。つまり、上記のデフォルトコールバックURLは機能しない。

どうして同じオブジェクトなの?

@provider = sorcery_get_provider provider_nameでproviderオブジェクトを取得しているが、このConfig.send(provider_name.to_sym)Config.githubと同じ意味。

1
2
3
4
5
def sorcery_get_provider(provider_name)
  return unless Config.external_providers.include?(provider_name.to_sym)

  Config.send(provider_name.to_sym)
end
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
module Sorcery
  module Controller
    module Submodules
      module External
        def self.included(base)
          base.send(:include, InstanceMethods)

          Config.module_eval do
            class << self
              attr_reader :external_providers
              attr_accessor :ca_file

              def external_providers=(providers)
                @external_providers = providers

                providers.each do |name|
                  class_eval <<-RUBY, __FILE__, __LINE__ + 1
                    def self.#{name}
                      @#{name} ||= Sorcery::Providers.const_get('#{name}'.to_s.classify).new
                    end
                  RUBY
                end
              end
            end
          end
        end
      end
    end
  end
end

この

1
2
3
def self.#{name}
  @#{name} ||= Sorcery::Providers.const_get('#{name}'.to_s.classify).new
end

は、つまり、

1
2
3
def self.github
  @github ||= Sorcery::Providers.const_get('github'.to_s.classify).new
end

なので、既にインスタンスが生成されて代入されているので、新しいインスタンスは生成しない。
(この||を消せば初期化問題は解決しそうだけど、影響の大きさが想像つかないのと、これ以上ハックするのってどうなんだ……)

注意

遷移先は、外部認証側のコールバックURL設定も必要。

GitHubの場合は↓
image

CALLBACK: http://example.com/path

GOOD: http://example.com/path
GOOD: http://example.com/path/subdir/other
BAD: http://example.com/bar
BAD: http://example.com/
BAD: http://example.com:8080/path
BAD: http://oauth.example.com:8080/path
BAD: http://example.org

Authorizing OAuth Apps - GitHub Docs

おわりに

login_url(_params, _session)_paramsでコントローラに渡された値は取得できるので、それをauthorize_urlに渡してこねこねすれば、もっとスマートに行けそう。
Githubモデルに独自のauthorize_urlを定義してオーバーライドするようにするとか。

Share on

END
END
@aiandrox

 
目次