LaunchSchool - An Online School for Developers /

Blog

Add OAuth to a Sorcery Based Rails App

In this tutorial, we will go through the steps to enable OAuth on a Ruby on Rails application using Sorcery for authentication. While we use Github as the OAuth provider in this tutorial, the steps for connecting to other OAuth providers should be very similar.

If you want to follow this tutorial but don’t have an existing Rails app, you can clone this app to start.

This repository contains the final app that is produced by following this entire tutorial.

Step 1. Register your application with Github

  1. Visit https://github.com/settings/applications/new

  2. Fill out the form, using http://localhost:3000 as the callback url (when you deploy the application you will need to change this to reflect the url of your application)

You have now been issued api keys for your application, you will need these in a little bit:

Step 2. Add oauth to your app

This guide follows much of the same steps as https://github.com/NoamB/sorcery/wiki/External, but shows how to allow logging in with Github without allowing user creation through Github.

Generate authentications table:

rails g sorcery:install external --migrations

This command creates:

1
2
3
4
5
6
7
8
9
10
class SorceryExternal < ActiveRecord::Migration
  def change
    create_table :authentications do |t|
      t.integer :user_id, :null => false
      t.string :provider, :uid, :null => false

      t.timestamps
    end
  end
end

Run your new migration:

rake db:migrate

Modify config/initializers/sorcery.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# config/initializers/sorcery.rb

# Here you can configure each submodule's features.
Rails.application.config.sorcery.configure do |config|
  ...

  # add :github to external_providers
  config.external_providers = [:github]

  ...

  # configure github using secrets (Rails 4.1, use environment variables in previous versions)
  config.github.key = "#{Rails.application.secrets.sorcery_github_key}"
  config.github.secret = "#{Rails.application.secrets.sorcery_github_secret}"
  config.github.callback_url = "#{Rails.application.secrets.sorcery_github_callback_url}"
  config.github.user_info_mapping = {:email => "name"}

  ...

  config.user_config do |user|
    ...

    user.authentications_class = Authentication

    ...
  end

  ...
end

Add the configuration to your secrets.yml file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# config/secrets.yml

development:
  ...
  sorcery_github_callback_url: http://0.0.0.0:3000/oauth/callback?provider=github
  sorcery_github_key: <%= ENV["SORCERY_GITHUB_KEY"] %>
  sorcery_github_secret: <%= ENV["SORCERY_GITHUB_SECRET"] %>

test:
  ...
  sorcery_github_key: <%= ENV["SORCERY_GITHUB_KEY"] %>
  sorcery_github_secret: <%= ENV["SORCERY_GITHUB_SECRET"] %>

# Do not keep production secrets in the repository,
# instead read values from the environment.
production:
  ...
  sorcery_github_callback_url: <%= ENV["SORCERY_GITHUB_CALLBACK_URL"] %>
  sorcery_github_key: <%= ENV["SORCERY_GITHUB_KEY"] %>
  sorcery_github_secret: <%= ENV["SORCERY_GITHUB_SECRET"] %>

Add the configuration to your environment variable manager (figaro if you followed step 1)

1
2
3
4
# config/application.yml

SORCERY_GITHUB_KEY: "403c26c099fcddc03a"
SORCERY_GITHUB_SECRET: "7381403c26c099fcdd4c03aa978f7dd7f9a1117403c26c099fcdd4c03a"

The User model needs to be associated with the Authentication model:

1
2
3
4
5
6
7
8
9
10
11
12
# app/models/user.rb

class User < ActiveRecord::Base
  authenticates_with_sorcery! do |config|
    config.authentications_class = Authentication
  end

  has_many :authentications, :dependent => :destroy
  accepts_nested_attributes_for :authentications

  ...
end
1
2
3
4
5
# app/models/authentication.rb

class Authentication < ActiveRecord::Base
  belongs_to :user
end

Create a has_linked_github? method in your User class:

1
2
3
4
5
6
7
8
9
# app/models/user.rb

class User < ActiveRecord::Base
  ...

  def has_linked_github?
    authentications.where(provider: 'github').present?
  end
end

Now it is time to add oauth links to your session links partial:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# app/views/shared/_session_links.html.erb

<div style="text-align: right;">
  <% if logged_in? %>
    Currently logged in as: <%= current_user.email %> |

    <% if current_user.has_linked_github? %>
      <%= link_to 'Unlink your Github account', delete_oauth_path('github'), method: :delete %>
    <% else %>
      <%= link_to 'Link your GitHub account', auth_at_provider_path(:provider => :github) %>
    <% end %> |

    <%= link_to 'Logout', logout_path %>
  <% else %>
    <%= link_to 'Login', login_path %> |
    <%= link_to 'Login using Github', auth_at_provider_path(provider: :github) %>
  <% end %>
</div>

Create a controller to handle oauth interactions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# app/controllers/oauths_controller.rb

class OauthsController < ApplicationController
  skip_before_filter :require_login
  before_filter :require_login, only: :destroy

  # sends the user on a trip to the provider,
  # and after authorizing there back to the callback url.
  def oauth
    login_at(auth_params[:provider])
  end

  # this is where all of the magic happens
  def callback
    # this will be set to 'github' when user is logging in via Github
    provider = auth_params[:provider]

    if @user = login_from(provider)
      # user has already linked their account with github

      flash[:notice] = "Logged in using #{provider.titleize}!"
      redirect_to root_path
    else
      # User has not linked their account with Github yet. If they are logged in,
      # authorize the account to be linked. If they are not logged in, require them
      # to sign in. NOTE: If you wanted to allow the user to register using oauth,
      # this section will need to be changed to be more like the wiki page that was
      # linked earlier.

      if logged_in?
        link_account(provider)
        redirect_to root_path
      else
        flash[:alert] = 'You are required to link your GitHub account before you can use this feature. You can do this by clicking "Link your Github account" after you sign in.'
        redirect_to login_path
      end
    end
  end

  # This is used to allow users to unlink their account from the oauth provider.
  #
  # In order to use this action you will need to include this route in your routes file:
  # delete "oauth/:provider" => "oauths#destroy", :as => :delete_oauth
  #
  # You will need to provide a 'provider' parameter to the action, create a link like this:
  # link_to 'unlink', delete_oauth_path('github'), method: :delete
  def destroy
    provider = params[:provider]

    authentication = current_user.authentications.find_by_provider(provider)
    if authentication.present?
      authentication.destroy
      flash[:notice] = "You have successfully unlinked your #{provider.titleize} account."
    else
      flash[:alert] = "You do not currently have a linked #{provider.titleize} account."
    end

    redirect_to root_path
  end

  private

  def link_account(provider)
    if @user = add_provider_to_user(provider)
      # If you want to store the user's Github login, which is required in order to interact with their Github account, uncomment the next line.
      # You will also need to add a 'github_login' string column to the users table.
      #
      # @user.update_attribute(:github_login, @user_hash[:user_info]['login'])
      flash[:notice] = "You have successfully linked your GitHub account."
    else
      flash[:alert] = "There was a problem linking your GitHub account."
    end
  end

  def auth_params
    params.permit(:code, :provider)
  end
end

Now let’s add the necessary routes to your routes file:

1
2
3
4
5
6
7
8
9
10
# config/routes.rb

...

post "oauth/callback" => "oauths#callback"
get "oauth/callback" => "oauths#callback" # for use with Github
get "oauth/:provider" => "oauths#oauth", :as => :auth_at_provider
delete "oauth/:provider" => "oauths#destroy", :as => :delete_oauth

root 'todos#index'

Step 3. Testing

You should always create adequate tests in your apps. Here is an example of the tests that I wrote to test the oauth functionality that we have added (using rspec):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# spec/controllers/oauths_controller_spec.rb

require 'spec_helper'

describe OauthsController do
  describe "#callback" do
    it 'logs in a linked user' do
      OauthsController.any_instance.should_receive(:login_from).with('github').and_return(Authentication.new)
      alice = Fabricate(:user)
      session[:user_id] = alice.id
      get :callback, provider: 'github', code: '123'

      expect(flash[:success]).to be_present
    end

    it 'links the users github account if they are logged in' do
      OauthsController.any_instance.should_receive(:login_from).and_return(false)
      OauthsController.any_instance.should_receive(:add_provider_to_user).and_return(Authentication.new(user_id: '123', uid: '123', provider: 'github'))
      controller.instance_variable_set(:@user_hash, { user_info: { 'login' => 'alice.smith' } })

      alice = Fabricate(:user)
      session[:user_id] = alice.id
      get :callback, provider: 'github', code: '123'

      expect(flash[:success]).to be_present
    end

    it 'saves the users github login when it links the account' do
      OauthsController.any_instance.should_receive(:login_from).and_return(false)
      OauthsController.any_instance.should_receive(:add_provider_to_user).and_return(Authentication.create(user_id: '123', uid: '123', provider: 'github'))
      controller.instance_variable_set(:@user_hash, { user_info: { 'login' => 'alice.smith' } })

      alice = Fabricate(:user)
      session[:user_id] = alice.id
      get :callback, provider: 'github', code: '123'

      expect(assigns(:user).login).to eq('alice.smith')
    end

    it 'displays an error if there is a problem linking github account' do
      OauthsController.any_instance.should_receive(:login_from).and_return(false)
      OauthsController.any_instance.should_receive(:add_provider_to_user).and_return(false)

      alice = Fabricate(:user)
      session[:user_id] = alice.id
      get :callback, provider: 'github', code: '123'

      expect(flash[:danger]).to be_present
    end

    it 'displays an error if user is not logged in and their github account is not linked' do
      OauthsController.any_instance.should_receive(:login_from).and_return(false)

      get :callback, provider: 'github', code: '123'
      expect(flash[:danger]).to be_present
    end
  end

  describe "DELETE destroy" do
    context 'user is logged in' do
      let(:alice) { Fabricate(:user) }
      before { session[:user_id] = alice.id }

      it 'deletes linked account for logged in user' do
        Fabricate(:authentication, user: alice)

        delete :destroy, provider: 'github'
        expect(alice.authentications.count).to eq(0)
      end

      it 'displays a success message after unlinking the users oauth account' do
        Fabricate(:authentication, user: alice)

        delete :destroy, provider: 'github'
        expect(flash[:success]).to be_present
      end

      it 'displays an error if there is no linked account for the logged in user' do
        delete :destroy, provider: 'github'
        expect(flash[:danger]).to be_present
      end
    end

    context 'user is not logged in' do
      it 'does not delete any linked accounts' do
        alice = Fabricate(:user)
        Fabricate(:authentication, user: alice)
        delete :destroy, provider: 'github'
        expect(alice.authentications.count).to eq(1)
      end
    end
  end
end

Step 4. Testing in development

You are now ready to give the app a whirl!

  1. In the terminal, start the Rails server: rails s
  2. In your browser, visit http://0.0.0.0:3000. NOTE: Do not use http://localhost:3000 because that does not match the Github callback url.
  3. Click “Login”.
  4. Click “Link your Github Account”
  5. Login to Github and authorize the app.
  6. Click “Logout”
  7. Click “Login with Github”.
  8. You are now logged in using Github!

Step 5. Deploying

When you deploy, do not forget to set the proper environment variables in your deployed environment. Also, you have to change the callback url on your Github application’s settings page. If you do not do one or both of these things, your users will be sent to a 404 page on Github when they try to link their account and also when they try to login using Github.