From a23a2f4a6270e92e069ce4635d17a93bb795d92f Mon Sep 17 00:00:00 2001 From: Patrick Mahoney Date: Tue, 2 Sep 2014 11:13:06 -0400 Subject: [PATCH 1/2] instagram_oauth (Oauth2) added to providers - registered as external submodule - implemenation in providers directory added mapping to create_user functionality --- lib/sorcery/controller/submodules/external.rb | 1 + lib/sorcery/providers/instagram.rb | 89 +++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 lib/sorcery/providers/instagram.rb diff --git a/lib/sorcery/controller/submodules/external.rb b/lib/sorcery/controller/submodules/external.rb index 07eefed7..f10d95b8 100644 --- a/lib/sorcery/controller/submodules/external.rb +++ b/lib/sorcery/controller/submodules/external.rb @@ -9,6 +9,7 @@ def self.included(base) require 'sorcery/providers/base' require 'sorcery/providers/facebook' + require 'sorcery/providers/instagram' require 'sorcery/providers/twitter' require 'sorcery/providers/vk' require 'sorcery/providers/linkedin' diff --git a/lib/sorcery/providers/instagram.rb b/lib/sorcery/providers/instagram.rb new file mode 100644 index 00000000..c8de3397 --- /dev/null +++ b/lib/sorcery/providers/instagram.rb @@ -0,0 +1,89 @@ +module Sorcery + module Providers + # This class adds support for OAuth with Instagram.com. + + class Instagram < Base + + include Protocols::Oauth2 + + + attr_accessor :access_permissions, :token_url, + :authorization_path, :user_info_path, + :scope, :user_info_fields + + + def initialize + @site = 'https://api.instagram.com' + @token_url = '/oauth/access_token' + @authorization_path = '/oauth/authorize/' + @user_info_path = '/v1/users/self' + super + end + + # provider implements method to build Oauth client + def login_url(params, session) + authorize_url + end + + + # @override of Base#authorize_url + def authorize_url(opts={}) + @scope = build_access_scope! + super(opts.merge(:token_url => @token_url)) + end + + # pass oauth2 param `code` provided by instgrm server + def process_callback(params, session) + args = {}.tap do |a| + a[:code] = params[:code] if params[:code] + end + get_access_token(args, token_url: @token_url, + client_id: @key, client_secret: @secret) + end + + + # see `user_info_mapping` in config/initializer, + # given `user_info_mapping` to specify + # {:db_attribute_name => 'instagram_attr_name'} + # so that Sorcery can build AR model from attr names + def get_user_hash(access_token) + _user_attrs = Hash.new + _user_attrs[:uid] = access_token['user']['id'] + _user_attrs[:user_info] = access_token['user'] + _user_attrs + end + + + private + + # build access scope attribute for instagram + # e.g. whether to access for `likes` or just basic + def build_access_scope! + valid = /\A(basic|comments|relationships|likes)$/ + + if !access_permissions.present? + _scopes = ["basic"] + elsif access_permissions.kind_of?(Array) + _scopes = access_permissions + .map(&:to_s) + .grep(valid) + elsif access_permissions.kind_of?(String) + _scopes = access_permissions + .split(/\W+/) + .grep(valid) + end + + # basic IS required + # b/c instagram fails on blank values + # and requires `basic` for any additional scope + _scopes.push('basic') + + _scopes.uniq.join(' ') + + end + + end + + end + +end \ No newline at end of file From b96ddd3142a441fb52e236dc54c7889031b80fe9 Mon Sep 17 00:00:00 2001 From: Patrick Mahoney Date: Sun, 7 Sep 2014 22:02:21 -0400 Subject: [PATCH 2/2] all tests for instagram auth - in internal app, named routes/methods for instagram - stubbed/tested changes in controller_oauth_2_spec - (note some reformatting changes, not logic changed) - in instagram provider, change logic for testing strategy Conflicts: spec/rails_app/app/controllers/sorcery_controller.rb --- lib/sorcery/providers/instagram.rb | 17 +++- spec/controllers/controller_oauth2_spec.rb | 89 ++++++++++++------- .../app/controllers/sorcery_controller.rb | 20 +++++ spec/rails_app/config/routes.rb | 3 + spec/shared_examples/user_shared_examples.rb | 2 +- 5 files changed, 97 insertions(+), 34 deletions(-) diff --git a/lib/sorcery/providers/instagram.rb b/lib/sorcery/providers/instagram.rb index c8de3397..5b8e7fad 100644 --- a/lib/sorcery/providers/instagram.rb +++ b/lib/sorcery/providers/instagram.rb @@ -46,10 +46,23 @@ def process_callback(params, session) # given `user_info_mapping` to specify # {:db_attribute_name => 'instagram_attr_name'} # so that Sorcery can build AR model from attr names + # + # NOTE: instead of just getting the user info + # from the access_token (which already returns them), + # testing strategy relies on querying user_info_path def get_user_hash(access_token) + call_api_params = { + :access_token => access_token.token, + :client_id => access_token[:client_id] + } + response = access_token.get( + "#{user_info_path}?#{call_api_params.to_param}" + ) + + _user_attrs = Hash.new - _user_attrs[:uid] = access_token['user']['id'] - _user_attrs[:user_info] = access_token['user'] + _user_attrs[:user_info] = JSON.parse(response.body)['data'] + _user_attrs[:uid] = _user_attrs[:user_info]['id'] _user_attrs end diff --git a/spec/controllers/controller_oauth2_spec.rb b/spec/controllers/controller_oauth2_spec.rb index 0b2b0a86..dc4238fe 100644 --- a/spec/controllers/controller_oauth2_spec.rb +++ b/spec/controllers/controller_oauth2_spec.rb @@ -137,7 +137,7 @@ expect(flash[:notice]).to eq "Success!" end - [:github, :google, :liveid, :vk].each do |provider| + [:github, :google, :liveid, :vk, :instagram].each do |provider| describe "with #{provider}" do @@ -190,11 +190,14 @@ end sorcery_reload!([:user_activation,:external], :user_activation_mailer => ::SorceryMailer) - sorcery_controller_property_set(:external_providers, [:facebook, :github, :google, :liveid, :vk]) + sorcery_controller_property_set(:external_providers, [:facebook, :github, :google, :liveid, :vk, :instagram]) sorcery_controller_external_property_set(:facebook, :key, "eYVNBjBDi33aa9GkA3w") sorcery_controller_external_property_set(:facebook, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") sorcery_controller_external_property_set(:facebook, :callback_url, "http://blabla.com") + sorcery_controller_external_property_set(:instagram, :key, "eYVNBjBDi33aa9GkA3w") + sorcery_controller_external_property_set(:instagram, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") + sorcery_controller_external_property_set(:instagram, :callback_url, "http://blabla.com") sorcery_controller_external_property_set(:github, :key, "eYVNBjBDi33aa9GkA3w") sorcery_controller_external_property_set(:github, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") sorcery_controller_external_property_set(:github, :callback_url, "http://blabla.com") @@ -235,7 +238,7 @@ expect(ActionMailer::Base.deliveries.size).to eq old_size end - [:github, :google, :liveid, :vk].each do |provider| + [:github, :google, :liveid, :vk, :instagram].each do |provider| it "does not send activation email to external users (#{provider})" do old_size = ActionMailer::Base.deliveries.size create_new_external_user provider @@ -266,7 +269,7 @@ end end - %w(facebook github google liveid vk).each do |provider| + %w(facebook github google liveid vk instagram).each do |provider| context "when #{provider}" do before(:each) do sorcery_controller_property_set(:register_login_time, true) @@ -306,7 +309,7 @@ let(:user) { double('user', id: 42) } - %w(facebook github google liveid vk).each do |provider| + %w(facebook github google liveid vk instagram).each do |provider| context "when #{provider}" do before(:each) do sorcery_model_property_set(:authentications_class, Authentication) @@ -343,31 +346,50 @@ def stub_all_oauth2_requests! access_token = double(OAuth2::AccessToken) allow(access_token).to receive(:token_param=) + allow(access_token).to receive(:[]).with(:client_id){ + "eYVNBjBDi33aa9GkA3w" + } response = double(OAuth2::Response) - allow(response).to receive(:body) { { - "id"=>"123", - "name"=>"Noam Ben Ari", - "first_name"=>"Noam", - "last_name"=>"Ben Ari", - "link"=>"http://www.facebook.com/nbenari1", - "hometown"=>{"id"=>"110619208966868", "name"=>"Haifa, Israel"}, - "location"=>{"id"=>"106906559341067", "name"=>"Pardes Hanah, Hefa, Israel"}, - "bio"=>"I'm a new daddy, and enjoying it!", - "gender"=>"male", - "email"=>"nbenari@gmail.com", - "timezone"=>2, - "locale"=>"en_US", - "languages"=>[{"id"=>"108405449189952", "name"=>"Hebrew"}, {"id"=>"106059522759137", "name"=>"English"}, {"id"=>"112624162082677", "name"=>"Russian"}], - "verified"=>true, - "updated_time"=>"2011-02-16T20:59:38+0000", - # response for VK auth - "response"=>[ - { - "uid"=>"123", - "first_name"=>"Noam", - "last_name"=>"Ben Ari" - } - ]}.to_json } + allow(response).to receive(:body) { + { + "id"=>"123", + "name"=>"Noam Ben Ari", + "first_name"=>"Noam", + "last_name"=>"Ben Ari", + "link"=>"http://www.facebook.com/nbenari1", + "hometown"=>{"id"=>"110619208966868", "name"=>"Haifa, Israel"}, + "location"=>{"id"=>"106906559341067", "name"=>"Pardes Hanah, Hefa, Israel"}, + "bio"=>"I'm a new daddy, and enjoying it!", + "gender"=>"male", + "email"=>"nbenari@gmail.com", + "timezone"=>2, + "locale"=>"en_US", + "languages"=>[{"id"=>"108405449189952", "name"=>"Hebrew"}, {"id"=>"106059522759137", "name"=>"English"}, {"id"=>"112624162082677", "name"=>"Russian"}], + "verified"=>true, + "updated_time"=>"2011-02-16T20:59:38+0000", + # response for VK auth + "response"=>[ + { + "uid"=>"123", + "first_name"=>"Noam", + "last_name"=>"Ben Ari" + } + ], + # stubbed response for instagram oauth2; + # thankfully keyed by 'data', so doesn't interfere + # with all the other stubs here + "data" =>{ + "username"=>"pnmahoney", + "bio"=>"turn WHAT down?", + "website"=>"", + "profile_picture"=> + "http://photos-d.ak.instagram.com/hphotos-ak-xpa1/10454121_417985815007395_867850883_a.jpg", + "full_name"=>"Patrick Mahoney", + "counts"=>{"media"=>2, "followed_by"=>100, "follows"=>71}, + "id"=>"123" + } + }.to_json + } allow(access_token).to receive(:get) { response } allow(access_token).to receive(:token) { "187041a618229fdaf16613e96e1caabc1e86e46bbfad228de41520e63fe45873684c365a14417289599f3" } # access_token params for VK auth @@ -376,10 +398,14 @@ def stub_all_oauth2_requests! end def set_external_property - sorcery_controller_property_set(:external_providers, [:facebook, :github, :google, :liveid, :vk]) + sorcery_controller_property_set(:external_providers, [:facebook, :github, :google, :liveid, :vk, :instagram]) sorcery_controller_external_property_set(:facebook, :key, "eYVNBjBDi33aa9GkA3w") sorcery_controller_external_property_set(:facebook, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") sorcery_controller_external_property_set(:facebook, :callback_url, "http://blabla.com") + sorcery_controller_external_property_set(:instagram, :key, "eYVNBjBDi33aa9GkA3w") + sorcery_controller_external_property_set(:instagram, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") + sorcery_controller_external_property_set(:instagram, :callback_url, "http://blabla.com") + sorcery_controller_external_property_set(:instagram, :scope, ['basic']) sorcery_controller_external_property_set(:github, :key, "eYVNBjBDi33aa9GkA3w") sorcery_controller_external_property_set(:github, :secret, "XpbeSdCoaKSmQGSeokz5qcUATClRW5u08QWNfv71N8") sorcery_controller_external_property_set(:github, :callback_url, "http://blabla.com") @@ -399,7 +425,8 @@ def provider_url(provider) github: "https://github.com/login/oauth/authorize?client_id=#{::Sorcery::Controller::Config.github.key}&display=&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=&state=", google: "https://accounts.google.com/o/oauth2/auth?client_id=#{::Sorcery::Controller::Config.google.key}&display=&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile&state=", liveid: "https://oauth.live.com/authorize?client_id=#{::Sorcery::Controller::Config.liveid.key}&display=&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=wl.basic+wl.emails+wl.offline_access&state=", - vk: "https://oauth.vk.com/authorize?client_id=#{::Sorcery::Controller::Config.vk.key}&display=&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=#{::Sorcery::Controller::Config.vk.scope}&state=" + vk: "https://oauth.vk.com/authorize?client_id=#{::Sorcery::Controller::Config.vk.key}&display=&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=#{::Sorcery::Controller::Config.vk.scope}&state=", + instagram: "https://api.instagram.com/oauth/authorize?client_id=#{::Sorcery::Controller::Config.instagram.key}&display=&redirect_uri=http%3A%2F%2Fblabla.com&response_type=code&scope=#{::Sorcery::Controller::Config.instagram.scope}&state=" }[provider] end diff --git a/spec/rails_app/app/controllers/sorcery_controller.rb b/spec/rails_app/app/controllers/sorcery_controller.rb index 2ce38b3b..e9ffa8c5 100644 --- a/spec/rails_app/app/controllers/sorcery_controller.rb +++ b/spec/rails_app/app/controllers/sorcery_controller.rb @@ -76,6 +76,10 @@ def test_http_basic_auth render text: 'HTTP Basic Auth' end + def login_at_test_instagram + login_at(:instagram) + end + def login_at_test_twitter login_at(:twitter) end @@ -110,6 +114,14 @@ def login_at_test_with_state login_at(:facebook, {state: 'bla'}) end + def test_login_from_instagram + if @user = login_from(:instagram) + redirect_to 'bla', notice: 'Success!' + else + redirect_to 'blu', alert: 'Failed!' + end + end + def test_login_from_twitter if @user = login_from(:twitter) redirect_to 'bla', notice: 'Success!' @@ -168,6 +180,14 @@ def test_login_from_jira end end + def test_return_to_with_external_instagram + if @user = login_from(:instagram) + redirect_back_or_to 'bla', notice: 'Success!' + else + redirect_to 'blu', alert: 'Failed!' + end + end + def test_return_to_with_external_twitter if @user = login_from(:twitter) redirect_back_or_to 'bla', notice: 'Success!' diff --git a/spec/rails_app/config/routes.rb b/spec/rails_app/config/routes.rb index 50465414..5984a7d1 100644 --- a/spec/rails_app/config/routes.rb +++ b/spec/rails_app/config/routes.rb @@ -16,6 +16,7 @@ get :test_add_second_provider get :test_return_to_with_external get :test_login_from + get :test_login_from_instagram get :test_login_from_twitter get :test_login_from_facebook get :test_login_from_github @@ -24,6 +25,7 @@ get :test_login_from_vk get :test_login_from_jira get :login_at_test + get :login_at_test_instagram get :login_at_test_twitter get :login_at_test_facebook get :login_at_test_github @@ -32,6 +34,7 @@ get :login_at_test_vk get :login_at_test_jira get :test_return_to_with_external + get :test_return_to_with_external_instagram get :test_return_to_with_external_twitter get :test_return_to_with_external_facebook get :test_return_to_with_external_github diff --git a/spec/shared_examples/user_shared_examples.rb b/spec/shared_examples/user_shared_examples.rb index 898dc5eb..a776b808 100644 --- a/spec/shared_examples/user_shared_examples.rb +++ b/spec/shared_examples/user_shared_examples.rb @@ -444,7 +444,7 @@ def self.matches?(crypted,*tokens) User.sorcery_adapter.delete_all end - [:facebook, :github, :google, :liveid].each do |provider| + [:facebook, :github, :google, :liveid, :instagram].each do |provider| it "does not send activation email to external users" do old_size = ActionMailer::Base.deliveries.size