├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── example ├── Gemfile └── config.ru ├── lib ├── omniauth-linkedin-oauth2.rb ├── omniauth-linkedin-oauth2 │ └── version.rb └── omniauth │ └── strategies │ └── linkedin.rb ├── omniauth-linkedin-oauth2.gemspec └── spec ├── omniauth └── strategies │ └── linkedin_spec.rb └── spec_helper.rb /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake 6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby 7 | 8 | name: Ruby 9 | 10 | on: 11 | push: 12 | branches: [ "master" ] 13 | pull_request: 14 | branches: [ "master" ] 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | test: 21 | 22 | runs-on: ubuntu-latest 23 | strategy: 24 | matrix: 25 | ruby-version: ['2.6', '2.7', '3.0'] 26 | 27 | steps: 28 | - uses: actions/checkout@v3 29 | - name: Set up Ruby 30 | # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, 31 | # change this to (see https://github.com/ruby/setup-ruby#versioning): 32 | # uses: ruby/setup-ruby@v1 33 | uses: ruby/setup-ruby@55283cc23133118229fd3f97f9336ee23a179fcf # v1.146.0 34 | with: 35 | ruby-version: ${{ matrix.ruby-version }} 36 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 37 | - name: Run tests 38 | run: bundle exec rake 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | .ruby-gemset 19 | .ruby-version -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2 4 | - 2.3 5 | - 2.4 6 | - 2.5 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in omniauth-linkedin-oauth2.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Décio Ferreira 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OmniAuth LinkedIn OAuth2 Strategy 2 | 3 | A LinkedIn OAuth2 strategy for OmniAuth. 4 | 5 | For more details, read the LinkedIn documentation: https://learn.microsoft.com/en-us/linkedin/shared/authentication/authentication 6 | 7 | > This version of Sign In with LinkedIn has been deprecated as of August 1, 2023. For all Sign In with LinkedIn implementations going forward, please refer to [Sign In with LinkedIn using OpenID Connect](https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2). 8 | 9 | ## Sign In with LinkedIn using OpenID Connect 10 | 11 | LinkedIn is now offering a way for your apps to authenticate members using OpenID Connect (OIDC). 12 | 13 | You should install the new `gem 'omniauth-linkedin-openid'` for this purpose. You can find it at 14 | [jclusso/omniauth-linkedin-openid](https://github.com/jclusso/omniauth-linkedin-openid). 15 | 16 | ## Installation 17 | 18 | Add this gem to your application's Gemfile: 19 | 20 | bundle add omniauth-linkedin-oauth2 21 | 22 | Or install it yourself as: 23 | 24 | $ gem install omniauth-linkedin-oauth2 25 | 26 | ## Upgrading 27 | 28 | This version is a major upgrade to the LinkedIn API version 2. As such, it switches from the soon to be no longer available `r_basicprofile` to `r_liteprofile`. This results in a much limited set of data that we can get from LinkedIn. 29 | 30 | Previous versions of this gem used the provider name `:linkedin_oauth2`. In order to provide a cleaner upgrade path for users who were previously using the OAuth 1.0 omniauth adapter for LinkedIn [https://github.com/skorks/omniauth-linkedin], this has been renamed to just `:linkedin`. 31 | 32 | Users who are upgrading from previous versions of this gem may need to update their Omniauth and/or Devise configurations to use the shorter provider name. 33 | 34 | ## Usage 35 | 36 | Register your application with LinkedIn to receive an API key: https://www.linkedin.com/developers/apps 37 | 38 | This is an example that you might put into a Rails initializer at `config/initializers/omniauth.rb`: 39 | 40 | ```ruby 41 | Rails.application.config.middleware.use OmniAuth::Builder do 42 | provider :linkedin, ENV['LINKEDIN_KEY'], ENV['LINKEDIN_SECRET'] 43 | end 44 | ``` 45 | 46 | You can now access the OmniAuth LinkedIn OAuth2 URL: `/auth/linkedin`. 47 | 48 | ## Granting Member Permissions to Your Application 49 | 50 | With the LinkedIn API, you have the ability to specify which permissions you want users to grant your application. 51 | For more details, read the LinkedIn documentation: https://developer.linkedin.com/docs/oauth2 52 | 53 | By default, omniauth-linkedin-oauth2 requests the following permissions: 54 | 55 | 'r_liteprofile r_emailaddress' 56 | 57 | You can configure the scope option: 58 | 59 | ```ruby 60 | provider :linkedin, ENV['LINKEDIN_KEY'], ENV['LINKEDIN_SECRET'], :scope => 'r_liteprofile' 61 | ``` 62 | 63 | ## Profile Fields 64 | 65 | When specifying which permissions you want to users to grant to your application, you will probably want to specify the array of fields that you want returned in the omniauth hash. The list of default fields is as follows: 66 | 67 | ```ruby 68 | ['id', 'first-name', 'last-name', 'picture-url', 'email-address'] 69 | ``` 70 | 71 | Here's an example of a possible configuration where the fields returned from the API are: id, first-name and last-name. 72 | 73 | ```ruby 74 | provider :linkedin, ENV['LINKEDIN_KEY'], ENV['LINKEDIN_SECRET'], :fields => ['id', 'first-name', 'last-name'] 75 | ``` 76 | 77 | To see a complete list of available fields, consult the LinkedIn documentation at: https://developer.linkedin.com/docs/fields 78 | 79 | ## Contributing 80 | 81 | 1. Fork it 82 | 2. Create your feature branch (`git checkout -b my-new-feature`) 83 | 3. Commit your changes (`git commit -am 'Add some feature'`) 84 | 4. Push to the branch (`git push origin my-new-feature`) 85 | 5. Create new Pull Request 86 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'sinatra' 4 | gem 'omniauth-linkedin-oauth2' 5 | -------------------------------------------------------------------------------- /example/config.ru: -------------------------------------------------------------------------------- 1 | # Sample app for LinkedIn OAuth2 Strategy 2 | # Make sure to setup the ENV variables LINKEDIN_KEY and LINKEDIN_SECRET 3 | # Run with "bundle exec rackup" 4 | 5 | require 'bundler/setup' 6 | require 'sinatra/base' 7 | require 'omniauth-linkedin-oauth2' 8 | 9 | class App < Sinatra::Base 10 | get '/' do 11 | redirect '/auth/linkedin' 12 | end 13 | 14 | get '/auth/:provider/callback' do 15 | content_type 'application/json' 16 | MultiJson.encode(request.env['omniauth.auth']) 17 | end 18 | 19 | get '/auth/failure' do 20 | content_type 'application/json' 21 | MultiJson.encode(request.env) 22 | end 23 | end 24 | 25 | use Rack::Session::Cookie, :secret => 'change_me' 26 | 27 | use OmniAuth::Builder do 28 | provider :linkedin, ENV['LINKEDIN_KEY'], ENV['LINKEDIN_SECRET'] 29 | end 30 | 31 | run App.new 32 | -------------------------------------------------------------------------------- /lib/omniauth-linkedin-oauth2.rb: -------------------------------------------------------------------------------- 1 | require "omniauth-linkedin-oauth2/version" 2 | require "omniauth/strategies/linkedin" 3 | -------------------------------------------------------------------------------- /lib/omniauth-linkedin-oauth2/version.rb: -------------------------------------------------------------------------------- 1 | module OmniAuth 2 | module LinkedInOAuth2 3 | VERSION = '1.0.1' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/omniauth/strategies/linkedin.rb: -------------------------------------------------------------------------------- 1 | require 'omniauth-oauth2' 2 | 3 | module OmniAuth 4 | module Strategies 5 | class LinkedIn < OmniAuth::Strategies::OAuth2 6 | option :name, 'linkedin' 7 | 8 | option :client_options, { 9 | :site => 'https://api.linkedin.com', 10 | :authorize_url => 'https://www.linkedin.com/oauth/v2/authorization?response_type=code', 11 | :token_url => 'https://www.linkedin.com/oauth/v2/accessToken' 12 | } 13 | 14 | option :scope, 'r_liteprofile r_emailaddress' 15 | option :fields, ['id', 'first-name', 'last-name', 'picture-url', 'email-address'] 16 | 17 | uid do 18 | raw_info['id'] 19 | end 20 | 21 | info do 22 | { 23 | :email => email_address, 24 | :first_name => localized_field('firstName'), 25 | :last_name => localized_field('lastName'), 26 | :picture_url => picture_url 27 | } 28 | end 29 | 30 | extra do 31 | { 32 | 'raw_info' => raw_info 33 | } 34 | end 35 | 36 | def callback_url 37 | full_host + script_name + callback_path 38 | end 39 | 40 | alias :oauth2_access_token :access_token 41 | 42 | def access_token 43 | ::OAuth2::AccessToken.new(client, oauth2_access_token.token, { 44 | :expires_in => oauth2_access_token.expires_in, 45 | :expires_at => oauth2_access_token.expires_at, 46 | :refresh_token => oauth2_access_token.refresh_token 47 | }) 48 | end 49 | 50 | def raw_info 51 | @raw_info ||= access_token.get(profile_endpoint).parsed 52 | end 53 | 54 | private 55 | 56 | def email_address 57 | if options.fields.include? 'email-address' 58 | fetch_email_address 59 | parse_email_address 60 | end 61 | end 62 | 63 | def fetch_email_address 64 | @email_address_response ||= access_token.get(email_address_endpoint).parsed 65 | end 66 | 67 | def parse_email_address 68 | return unless email_address_available? 69 | 70 | @email_address_response['elements'].first['handle~']['emailAddress'] 71 | end 72 | 73 | def email_address_available? 74 | @email_address_response['elements'] && 75 | @email_address_response['elements'].is_a?(Array) && 76 | @email_address_response['elements'].first && 77 | @email_address_response['elements'].first['handle~'] 78 | end 79 | 80 | def fields_mapping 81 | { 82 | 'id' => 'id', 83 | 'first-name' => 'firstName', 84 | 'last-name' => 'lastName', 85 | 'picture-url' => 'profilePicture(displayImage~:playableStreams)' 86 | } 87 | end 88 | 89 | def fields 90 | options.fields.each.with_object([]) do |field, result| 91 | result << fields_mapping[field] if fields_mapping.has_key? field 92 | end 93 | end 94 | 95 | def localized_field field_name 96 | return unless localized_field_available? field_name 97 | 98 | raw_info[field_name]['localized'][field_locale(field_name)] 99 | end 100 | 101 | def field_locale field_name 102 | "#{ raw_info[field_name]['preferredLocale']['language'] }_" \ 103 | "#{ raw_info[field_name]['preferredLocale']['country'] }" 104 | end 105 | 106 | def localized_field_available? field_name 107 | raw_info[field_name] && raw_info[field_name]['localized'] 108 | end 109 | 110 | def picture_url 111 | return unless picture_available? 112 | 113 | picture_references.last['identifiers'].first['identifier'] 114 | end 115 | 116 | def picture_available? 117 | raw_info['profilePicture'] && 118 | raw_info['profilePicture']['displayImage~'] && 119 | picture_references 120 | end 121 | 122 | def picture_references 123 | raw_info['profilePicture']['displayImage~']['elements'] 124 | end 125 | 126 | def email_address_endpoint 127 | '/v2/emailAddress?q=members&projection=(elements*(handle~))' 128 | end 129 | 130 | def profile_endpoint 131 | "/v2/me?projection=(#{ fields.join(',') })" 132 | end 133 | 134 | def token_params 135 | super.tap do |params| 136 | params.client_secret = options.client_secret 137 | end 138 | end 139 | end 140 | end 141 | end 142 | 143 | OmniAuth.config.add_camelization 'linkedin', 'LinkedIn' 144 | -------------------------------------------------------------------------------- /omniauth-linkedin-oauth2.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'omniauth-linkedin-oauth2/version' 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = "omniauth-linkedin-oauth2" 8 | gem.version = OmniAuth::LinkedInOAuth2::VERSION 9 | gem.authors = ["Décio Ferreira"] 10 | gem.email = ["decio.ferreira@decioferreira.com"] 11 | gem.description = %q{A LinkedIn OAuth2 strategy for OmniAuth.} 12 | gem.summary = %q{A LinkedIn OAuth2 strategy for OmniAuth.} 13 | gem.homepage = "https://github.com/decioferreira/omniauth-linkedin-oauth2" 14 | gem.license = "MIT" 15 | 16 | gem.files = `git ls-files`.split($/) 17 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 18 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 19 | gem.require_paths = ["lib"] 20 | 21 | gem.add_runtime_dependency 'omniauth-oauth2' 22 | 23 | gem.add_development_dependency 'bundler' 24 | gem.add_development_dependency 'rake' 25 | 26 | gem.add_development_dependency 'rspec' 27 | gem.add_development_dependency 'simplecov' 28 | end 29 | -------------------------------------------------------------------------------- /spec/omniauth/strategies/linkedin_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'omniauth-linkedin-oauth2' 3 | 4 | describe OmniAuth::Strategies::LinkedIn do 5 | subject { OmniAuth::Strategies::LinkedIn.new(nil) } 6 | 7 | it 'adds camelization for itself' do 8 | expect(OmniAuth::Utils.camelize('linkedin')).to eq('LinkedIn') 9 | end 10 | 11 | describe '#client' do 12 | it 'has correct LinkedIn site' do 13 | expect(subject.client.site).to eq('https://api.linkedin.com') 14 | end 15 | 16 | it 'has correct `authorize_url`' do 17 | expect(subject.client.options[:authorize_url]).to eq('https://www.linkedin.com/oauth/v2/authorization?response_type=code') 18 | end 19 | 20 | it 'has correct `token_url`' do 21 | expect(subject.client.options[:token_url]).to eq('https://www.linkedin.com/oauth/v2/accessToken') 22 | end 23 | end 24 | 25 | describe '#callback_path' do 26 | it 'has the correct callback path' do 27 | expect(subject.callback_path).to eq('/auth/linkedin/callback') 28 | end 29 | end 30 | 31 | describe '#uid' do 32 | before :each do 33 | allow(subject).to receive(:raw_info) { Hash['id' => 'uid'] } 34 | end 35 | 36 | it 'returns the id from raw_info' do 37 | expect(subject.uid).to eq('uid') 38 | end 39 | end 40 | 41 | describe '#info / #raw_info' do 42 | let(:access_token) { instance_double OAuth2::AccessToken } 43 | 44 | let(:parsed_response) { Hash[:foo => 'bar'] } 45 | 46 | let(:profile_endpoint) { '/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))' } 47 | let(:email_address_endpoint) { '/v2/emailAddress?q=members&projection=(elements*(handle~))' } 48 | 49 | let(:email_address_response) { instance_double OAuth2::Response, parsed: parsed_response } 50 | let(:profile_response) { instance_double OAuth2::Response, parsed: parsed_response } 51 | 52 | before :each do 53 | allow(subject).to receive(:access_token).and_return access_token 54 | 55 | allow(access_token).to receive(:get) 56 | .with(email_address_endpoint) 57 | .and_return(email_address_response) 58 | 59 | allow(access_token).to receive(:get) 60 | .with(profile_endpoint) 61 | .and_return(profile_response) 62 | end 63 | 64 | it 'returns parsed responses using access token' do 65 | expect(subject.info).to have_key :email 66 | expect(subject.info).to have_key :first_name 67 | expect(subject.info).to have_key :last_name 68 | expect(subject.info).to have_key :picture_url 69 | 70 | expect(subject.raw_info).to eq({ :foo => 'bar' }) 71 | end 72 | end 73 | 74 | describe '#extra' do 75 | let(:raw_info) { Hash[:foo => 'bar'] } 76 | 77 | before :each do 78 | allow(subject).to receive(:raw_info).and_return raw_info 79 | end 80 | 81 | specify { expect(subject.extra['raw_info']).to eq raw_info } 82 | end 83 | 84 | describe '#access_token' do 85 | let(:expires_in) { 3600 } 86 | let(:expires_at) { 946688400 } 87 | let(:token) { 'token' } 88 | let(:refresh_token) { 'refresh_token' } 89 | let(:access_token) do 90 | instance_double OAuth2::AccessToken, :expires_in => expires_in, 91 | :expires_at => expires_at, :token => token, :refresh_token => refresh_token 92 | end 93 | 94 | before :each do 95 | allow(subject).to receive(:oauth2_access_token).and_return access_token 96 | end 97 | 98 | specify { expect(subject.access_token.expires_in).to eq expires_in } 99 | specify { expect(subject.access_token.expires_at).to eq expires_at } 100 | end 101 | 102 | describe '#authorize_params' do 103 | describe 'scope' do 104 | before :each do 105 | allow(subject).to receive(:session).and_return({}) 106 | end 107 | 108 | it 'sets default scope' do 109 | expect(subject.authorize_params['scope']).to eq('r_liteprofile r_emailaddress') 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start 3 | 4 | # This file was generated by the `rspec --init` command. Conventionally, all 5 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 6 | # Require this file using `require "spec_helper"` to ensure that it is only 7 | # loaded once. 8 | # 9 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 10 | RSpec.configure do |config| 11 | config.run_all_when_everything_filtered = true 12 | config.filter_run :focus 13 | 14 | # Run specs in random order to surface order dependencies. If you find an 15 | # order dependency and want to debug it, you can fix the order by providing 16 | # the seed, which is printed after each run. 17 | # --seed 1234 18 | config.order = 'random' 19 | end 20 | --------------------------------------------------------------------------------