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.
Written by Troy Martin
Related protips
4 Responses
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
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.
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
Hey,
Why were you using https://github.com/Devato/omniauth-linkedin-oauth2 and not its parent https://github.com/decioferreira/omniauth-linkedin-oauth2 ?