Last Updated: September 12, 2018
·
9.747K
· tmartin314

Linkedin OAuth2 Login for Rails

There is a great gem for authenticating with Linked named: omniauth-linkedin-oauth2

There was an issue with the original gem so I forked it and adjusted it, so feel free to fork it or use mine.

Create Linkedin App

First things first head to https://www.linkedin.com/developer/apps and create an app to authenticate with.

Imporant note on setting the URL:

You must set up callback url's for each environment in the Linkedin App for the callbacks to work properly.

For development:

http://appname.dev/auth/linkedin/callback

For production:

https://appname.com/auth/linkedin/callback

1. Start by adding the gem

gem 'omniauth-linkedin-oauth2', git: 'https://github.com/Devato/omniauth-linkedin-oauth2.git'

2. Then configure the initializer:

#config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :linkedin, ENV['LINKEDIN_CLIENT_ID'], ENV['LINKEDIN_CLIENT_SECRET'], secure_image_url: true
end

Now let’s return to the Callback URL. This is the URL where a user will be redirected to inside the app after successful authentication and approved authorization (the request will also contain user’s data and token). All OmniAuth strategies expect the callback URL to equal to “/auth/:provider/callback”. :provider takes the name of the strategy (“twitter”, “facebook”, “linkedin”, etc.) as listed in the initializer.

3. Add the routes

#config/routes.rb
get '/auth/:provider/callback', to: 'oauth#callback', as: 'oauth_callback'
get '/auth/failure', to: 'oauth#failure', as: 'oauth_failure'

4. A link to send the request off to Linkedin, (this obviously won't work yet).

= link_to 'Register with Linkedin', '/auth/linkedin' 

5. Prepare to store the data

You can set store the data any way you like. I created a separate model for to hold everything. Here's the migration:

class CreateOauthAccounts < ActiveRecord::Migration
  def change
    create_table :oauth_accounts do |t|
      t.belongs_to :user, index: true, foreign_key: true
      t.string :provider
      t.string :uid
      t.string :image_url
      t.string :profile_url
      t.string :access_token
      t.text :raw_data
      t.timestamps null: false
    end
  end
end

6. Process the callback.

I generated an Oauth controller to keep things separate:

#app/controllers/oauth_controller.rb
class OauthController < ApplicationController

  def callback
    begin
      oauth = OauthService.new(request.env['omniauth.auth'])
      if oauth_account = oauth.create_oauth_account!
          ...
          redirect_to Config.provider_login_path
      end
    rescue => e
      flash[:alert] = "There was an error while trying to authenticate your account."
      redirect_to register_path
    end
  end

  def failure
    flash[:alert] = "There was an error while trying to authenticate your account."
    redirect_to register_path
  end

end

You'll notice there are two methods to handle the response from linkedin. callback and failure. Call back is processing the request.env['omniauth.auth'] data and creating the records as necessary. OauthService is a custom service being used to actually process the data.

7. The service that processes callback data

I passed this processing off to a Service class to keep thing organized:

#app/services/oauth_service.rb
class OauthService
  attr_reader :auth_hash

  def initialize(auth_hash)
    @auth_hash = auth_hash
  end

  def create_oauth_account!
    unless oauth_account = OauthAccount.where(uid: @auth_hash[:uid]).first
      oauth_account = OauthAccount.create!(oauth_account_params)
    end
    oauth_account
  end

private

  def oauth_account_params
    { uid: @auth_hash[:uid],
      provider: @auth_hash[:provider],
      image_url: @auth_hash[:info][:image],
      profile_url: @auth_hash[:info][:urls][:public_profile],
      raw_data: @auth_hash[:extra][:raw_info].to_json }
  end

end

There are many different ways to accomplish this, but this felt right and was pretty straight forward to test.

ADD RSPEC TEST COVERAGE

There are a few nuances of testing this process, and here's what worked in getting mocking set up:

1. Setup macro for mocking the response

#spec/support/omniauth_macros.rb
module OmniauthMacros
  def mock_auth_hash
    # The mock_auth configuration allows you to set per-provider (or default)
    # authentication hashes to return during integration testing.
    OmniAuth.config.mock_auth[:linkedin] = OmniAuth::AuthHash.new({
      'provider' => 'linkedin',
      'uid' => '123545',
      'info' => {
        'name' => 'mockuser',
        'image' => 'mock_user_thumbnail_url',
        'first_name' => 'john',
        'last_name' => 'doe',
        'email' => 'john@doe.com',
        'urls' => {
          'public_profile' => 'http://test.test/public_profile'
        }
      },
      'credentials' => {
        'token' => 'mock_token',
        'secret' => 'mock_secret'
      },
      'extra' => {
        'raw_info' => '{"json":"data"}'
      }
    })
  end
end

2. Include this macro in your RSpec config:

#spec/rails_helper.rb
RSpec.configure do |config|
  config.include(OmniauthMacros)
end

Then underneath the configure block set OmniAuth to test mode:

OmniAuth.config.test_mode = true

3. Add Request Spec:

#spec/requests/registration_spec.rb

require "rails_helper"

describe "home#register as a provider", :type => :request do

  it "redirects to oauth#callback" do
    get '/auth/linkedin'
    expect(response).to redirect_to(oauth_callback_path(:linkedin))
  end

end

Because OmniAuth is in test mode it should redirect to callback.

4. OauthController spec:

#spec/controllers/oauth_controller_spec.rb

require 'rails_helper'

describe OauthController, type: :controller do

  before(:each) do
    mock_auth_hash
    request.env['omniauth.auth'] = OmniAuth.config.mock_auth[:linkedin]
  end

  after(:each) do
    OmniAuth.config.mock_auth[:linkedin] = nil
  end

  describe 'GET #callback' do
    it 'expects omniauth.auth to be be_truthy' do
      get :callback, provider: :linkedin
      expect(request.env['omniauth.auth']).to be_truthy
    end

    it 'completes the registration process successfully' do
      get :callback, provider: :linkedin
      expect(page).to redirect_to Config.provider_login_path
    end

    it 'creates an oauth_account record' do
      expect{
        get :callback, provider: :linkedin
      }.to change { OauthAccount.count }.by(1)
    end

  end

end

You'll notice calling mock_auth_hash and setting the request.env['omniauth.auth'] prepares the mock data to test against.

These basic tests ensure that the mock is working properly, that we're redirected to the proper route, and that the OauthAccount record was created properly.

There are many different ways to achieve these same results, but this is what worked well for us, and hopefully it's a good reference for anyone needed to accomplish the same things.

4 Responses
Add your response

Hey Troy,

Thanks for the great walkthrough! I had a couple issues getting the specs to pass.

When running the Oauth controller spec, the callback method check successfully requests the omniauth.auth. The registration check spits out an error saying:
NameError:
undefined local variable or method `page'

While the Oauth account record check spits out an error:
NameError:
uninitialized constant OauthAccount

Any recommendations on resolving these errors?

Thanks,
Geezy

over 1 year ago ·

I just have to say this is the most comprehensive text guide that I've seen. As a beginner, no other site says where to put individual files or snippets of code. A few notes/questions.

Part 6 is where I'm having the most difficulty. This line in particular:
if oauthaccount = oauth.createoauth_account!

First off, if statements should use double equals to compare. "==" Not sure if the single equals was intentional. Then there's a bang in a conditional, which is very odd. I'm not sure what this line is suppose to do, and I'm pretty sure there's at least one error here.

Second, I'm not sure what type of logic is suppose to go where there is "..." Is this where you assign oauth_account's value to the values already in use in YourApp's model (or vis versa)? Do you run another method here?

Part 7 also has a single equals in a conditional:
unless oauthaccount = OauthAccount.where(uid: @authhash[:uid]).first

Like I said though, this was way more helpful than the githubs for any of the linkedin gems and really the best I've been able to find.

over 1 year ago ·

Hi Troy,
Thank you for this tutorial.
I am following your tutorial on a basic rails 5 app, and unfortunately I keep getting an authentication error (OAuth2:Error)
invalidrequest: missing required parameters, includes an invalid parameter value, parameter more than once. : code {"errordescription":"missing required parameters, includes an invalid parameter value, parameter more than once. : code","error":"invalid_request"}

i am simply trying to call /auth/linkedin after step 2 and it keeps throwing this nasty error. Can you please provide some help?

Thanks,
Puja

over 1 year ago ·