diff --git a/lib/omniauth/strategies/linkedin.rb b/lib/omniauth/strategies/linkedin.rb index 1b16934..8f56a1c 100755 --- a/lib/omniauth/strategies/linkedin.rb +++ b/lib/omniauth/strategies/linkedin.rb @@ -3,6 +3,23 @@ module OmniAuth module Strategies class LinkedIn < OmniAuth::Strategies::OAuth2 + V1_TO_V2_FIELD_MAP = { + 'id' => 'id', + 'email-address' => nil, + 'first-name' => 'localizedFirstName', + 'last-name' => 'localizedLastName', + 'headline' => 'headline', + 'location' => nil, + 'industry' => 'industryName', + 'picture-url' => 'profilePicture(displayImage~:playableStreams)', + 'public-profile-url' => 'vanityName' + } + + PROFILE_ENDPOINT = { + 'v1' => '/v1/people/~', + 'v2' => '/v2/me' + } + # Give your strategy a name. option :name, 'linkedin' @@ -16,6 +33,7 @@ class LinkedIn < OmniAuth::Strategies::OAuth2 option :scope, 'r_basicprofile r_emailaddress' option :fields, ['id', 'email-address', 'first-name', 'last-name', 'headline', 'location', 'industry', 'picture-url', 'public-profile-url'] + option :api_version, 'v1' # These are called after authentication has succeeded. If # possible, you should try to set the UID without making @@ -25,19 +43,35 @@ class LinkedIn < OmniAuth::Strategies::OAuth2 uid { raw_info['id'] } info do - { - :name => user_name, - :email => raw_info['emailAddress'], - :nickname => user_name, - :first_name => raw_info['firstName'], - :last_name => raw_info['lastName'], - :location => raw_info['location'], - :description => raw_info['headline'], - :image => raw_info['pictureUrl'], - :urls => { - 'public_profile' => raw_info['publicProfileUrl'] + if options.api_version == "v1" + { + :name => user_name, + :email => raw_info['emailAddress'], + :nickname => user_name, + :first_name => raw_info['firstName'], + :last_name => raw_info['lastName'], + :location => raw_info['location'], + :description => raw_info['headline'], + :image => raw_info['pictureUrl'], + :urls => { + 'public_profile' => raw_info['publicProfileUrl'] + } } - } + elsif options.api_version == "v2" + { + :name => user_name, + :email => nil, + :nickname => user_name, + :first_name => raw_info['localizedFirstName'], + :last_name => raw_info['localizedLastName'], + :location => nil, + :description => localized_field(raw_info['headline']), + :image => profile_picture, + :urls => { + 'public_profile' => "https://www.linkedin.com/in/#{raw_info['vanityName']}" + } + } + end end extra do @@ -60,21 +94,59 @@ def access_token end def raw_info - @raw_info ||= access_token.get("/v1/people/~:(#{option_fields.join(',')})?format=json").parsed + @raw_info ||= access_token.get(profile_endpoint).parsed end private def option_fields fields = options.fields - fields.map! { |f| f == "picture-url" ? "picture-url;secure=true" : f } if !!options[:secure_image_url] - fields + fields.map! do |f| + if options.api_version == 'v2' + V1_TO_V2_FIELD_MAP.fetch(f,f) + elsif !!options[:secure_image_url] && f == 'picture-url' + "picture-url;secure=true" + else + f + end + end + fields.compact + end + + def localized_field(field) + return nil unless field + locale = "#{field['preferredLocale']['language']}_#{field['preferredLocale']['country']}" + field['localized'][locale] + end + + def profile_picture + return nil if raw_info['profilePicture'].to_s.empty? + raw_info['profilePicture']['displayImage~']['elements'].first['identifiers'].first['identifier'] + end + + def first_name + raw_info['firstName'] || raw_info['localizedFirstName'] + end + + def last_name + raw_info['lastName'] || raw_info['localizedLastName'] end def user_name - name = "#{raw_info['firstName']} #{raw_info['lastName']}".strip + name = "#{first_name} #{last_name}" name.empty? ? nil : name end + + def profile_endpoint + suffix = case options.api_version + when 'v1' + ":(#{option_fields.join(',')})?format=json" + when 'v2' + "?projection=(#{option_fields.join(',')})" + end + + PROFILE_ENDPOINT[options.api_version] + suffix + end end end end diff --git a/spec/omniauth/strategies/linkedin_spec.rb b/spec/omniauth/strategies/linkedin_spec.rb index ffc299a..e731888 100755 --- a/spec/omniauth/strategies/linkedin_spec.rb +++ b/spec/omniauth/strategies/linkedin_spec.rb @@ -43,16 +43,34 @@ allow(subject).to receive(:raw_info) { {} } end - context 'and therefore has all the necessary fields' do - it { expect(subject.info).to have_key :name } - it { expect(subject.info).to have_key :email } - it { expect(subject.info).to have_key :nickname } - it { expect(subject.info).to have_key :first_name } - it { expect(subject.info).to have_key :last_name } - it { expect(subject.info).to have_key :location } - it { expect(subject.info).to have_key :description } - it { expect(subject.info).to have_key :image } - it { expect(subject.info).to have_key :urls } + context 'api_version is v1' do + context 'and therefore has all the necessary fields' do + it { expect(subject.info).to have_key :name } + it { expect(subject.info).to have_key :email } + it { expect(subject.info).to have_key :nickname } + it { expect(subject.info).to have_key :first_name } + it { expect(subject.info).to have_key :last_name } + it { expect(subject.info).to have_key :location } + it { expect(subject.info).to have_key :description } + it { expect(subject.info).to have_key :image } + it { expect(subject.info).to have_key :urls } + end + end + + context 'api_version is v2' do + before :each do + subject.stub(:options => double('options', :api_version => 'v2').as_null_object) + end + + context 'and therefore has all the necessary fields' do + it { expect(subject.info).to have_key :name } + it { expect(subject.info).to have_key :nickname } + it { expect(subject.info).to have_key :first_name } + it { expect(subject.info).to have_key :last_name } + it { expect(subject.info).to have_key :description } + it { expect(subject.info).to have_key :image } + it { expect(subject.info).to have_key :urls } + end end end @@ -75,16 +93,34 @@ describe '#raw_info' do before :each do - access_token = double('access token') - response = double('response', :parsed => { :foo => 'bar' }) - expect(access_token).to receive(:get).with("/v1/people/~:(baz,qux)?format=json").and_return(response) - allow(subject).to receive(:option_fields) { ['baz', 'qux'] } - allow(subject).to receive(:access_token) { access_token } end - it 'returns parsed response from access token' do - expect(subject.raw_info).to eq({ :foo => 'bar' }) + context 'api_version is v1' do + before :each do + response = double('response', :parsed => { :foo => 'bar' }) + access_token = double('access token') + expect(access_token).to receive(:get).with("/v1/people/~:(baz,qux)?format=json").and_return(response) + allow(subject).to receive(:access_token) { access_token } + end + + it 'returns parsed response from access token' do + expect(subject.raw_info).to eq({ :foo => 'bar' }) + end + end + + context 'api_version is v2' do + before :each do + response = double('response', :parsed => { :foo => 'bar' }) + access_token = double('access token') + expect(access_token).to receive(:get).with("/v2/me?projection=(baz,qux)").and_return(response) + allow(subject).to receive(:access_token) { access_token } + subject.stub(:options => double('options', :api_version => 'v2').as_null_object) + end + + it 'returns parsed response from access token' do + expect(subject.raw_info).to eq({ :foo => 'bar' }) + end end end @@ -101,21 +137,65 @@ end describe '#option_fields' do - it 'returns options fields' do - subject.stub(:options => double('options', :fields => ['foo', 'bar']).as_null_object) - expect(subject.send(:option_fields)).to eq(['foo', 'bar']) - end + context 'api_version is v1' do + it 'returns options fields' do + subject.stub(:options => double('options', :fields => ['foo', 'bar'], :api_version => 'v1').as_null_object) + expect(subject.send(:option_fields)).to eq(['foo', 'bar']) + end + + it 'http avatar image by default' do + subject.stub(:options => double('options', :fields => ['picture-url'], :api_version => 'v1')) + allow(subject.options).to receive(:[]).with(:secure_image_url).and_return(false) + expect(subject.send(:option_fields)).to eq(['picture-url']) + end - it 'http avatar image by default' do - subject.stub(:options => double('options', :fields => ['picture-url'])) - allow(subject.options).to receive(:[]).with(:secure_image_url).and_return(false) - expect(subject.send(:option_fields)).to eq(['picture-url']) + it 'https avatar image if secure_image_url truthy' do + subject.stub(:options => double('options', :fields => ['picture-url'], :api_version => 'v1')) + allow(subject.options).to receive(:[]).with(:secure_image_url).and_return(true) + expect(subject.send(:option_fields)).to eq(['picture-url;secure=true']) + end end - it 'https avatar image if secure_image_url truthy' do - subject.stub(:options => double('options', :fields => ['picture-url'])) - allow(subject.options).to receive(:[]).with(:secure_image_url).and_return(true) - expect(subject.send(:option_fields)).to eq(['picture-url;secure=true']) + context 'api_version is v2' do + it 'returns options fields' do + subject.stub(:options => double('options', :fields => ['foo', 'bar'], :api_version => 'v2').as_null_object) + expect(subject.send(:option_fields)).to eq(['foo', 'bar']) + end + + it 'converts picture-url to the profilePicture projection' do + subject.stub(:options => double('options', :fields => ['picture-url'], :api_version => 'v2').as_null_object) + expect(subject.send(:option_fields)).to eq(['profilePicture(displayImage~:playableStreams)']) + end + + it 'converts first-name to the localizedFirstName projection' do + subject.stub(:options => double('options', :fields => ['first-name'], :api_version => 'v2').as_null_object) + expect(subject.send(:option_fields)).to eq(['localizedFirstName']) + end + + it 'converts last-name to the localizedLastName projection' do + subject.stub(:options => double('options', :fields => ['last-name'], :api_version => 'v2').as_null_object) + expect(subject.send(:option_fields)).to eq(['localizedLastName']) + end + + it 'converts industry to the industryName projection' do + subject.stub(:options => double('options', :fields => ['industry'], :api_version => 'v2').as_null_object) + expect(subject.send(:option_fields)).to eq(['industryName']) + end + + it 'converts public-profile-url to the vanityName projection' do + subject.stub(:options => double('options', :fields => ['public-profile-url'], :api_version => 'v2').as_null_object) + expect(subject.send(:option_fields)).to eq(['vanityName']) + end + + it 'ignores email-address' do + subject.stub(:options => double('options', :fields => ['email-address'], :api_version => 'v2').as_null_object) + expect(subject.send(:option_fields)).to eq([]) + end + + it 'ignores location' do + subject.stub(:options => double('options', :fields => ['location'], :api_version => 'v2').as_null_object) + expect(subject.send(:option_fields)).to eq([]) + end end end end