Facebook OAuth Using OmniAuth and Rails 4

- - posted in facebook,, oauth,, omniauth

Allowing Facebook logins in Rails 4

I had to copy and paste in information from a variety of sources to end up with something I liked at the time of this writing. Since I want to walk my students in Launchpad Code Academy through this process, I figured I’d make this walkthrough public. It may help newcomers for quite some time to come!

About this solution

We are going to be using a server-side OAuth authorization. The mechanism uses some JavaScript checks and interfaces with the JS Facebook API, but ultimately the connection is made on the server-side.

You don’t necessarily have to be using Devise, but if you’re not, our tips about how to leverage this approach for Facebook login may not be helpful.

With a little extra work, you can allow users that have attached their Facebook account to log in with Facebook. You might even be able to adapt this approach to allowing users to create accounts with a single click. But you won’t like that as much when you want to get Twitter involved, since Twitter doesn’t send an email address in its OAuth callback. It’s best to have existing users affiliate their accounts and then providing login as a convenience.

Getting started

Step 1: Installing gems

Include these gems in your Gemfile:

Gemfile
1
2
gem "omniauth"
gem 'omniauth-facebook', '1.4.0'

Install them in your terminal with the bundle command:

Terminal
1
bundle install

Step 2

Per the usage instructions for omniauth-facebook, create a new file in config/initializers named omniauth.rb:

config/initializers/omniauth.rb
1
2
3
4
5
OmniAuth.config.logger = Rails.logger

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :facebook, ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET']
end

The config/initializers directory is designed for use on Rails startup to configure how libraries are intended to operate. In this case, we want our Rails application to know that we are using Omniauth, and are configuring the Facebook provider to reference environment variables to run.

To set these environment variables properly, we’ll need to go get some info from Facebook.

Creating and configuring a Facbook App

Step 1: An app

I honestly don’t remember what it’s like to register for a Facebook developer account, other than it should be free, and it should happen at the Facebook Developers site.

When you’ve registered properly, there should be an “Apps” tab in your navigation. Click on that to travel to your list of apps.

Click “Create New App”. You should pick a name that indicates you are doing testing. You can place it in a namespace, if you like, but that’s optional.

Mine looked like this

Click “Continue”, and you should be brought to an application screen that contains your App ID (or API Key) and your App Secret. Note those, but we’ll need to fill in some information here.

In the “App Domains” box, put ‘localhost’

Click the green check on “Website with Facebook Login”, and fill that in with http://localhost:5000/. My students (or anyone who uses my Rails templates) will be able to use this. This value may be different for you— make sure that it matches whatever you use in development. localhost:3000 would be common if you are using rails s and appname.dev would be likely if you were using Pow.

Mine looked like this

Step 2: Installing application details

Students in my class use foreman to run their development environment. If you won’t, perhaps you can consider dotenv, to load env variables from files for you? It’s important not to commit sensitive information like this Facebook App Secret to your repository. Note the difficulties and caveats, should you ever need to remove sensitive data. Usage of foreman or dotenv can help with that.

If you are using foreman, you can add these files to your .env file (which is not in version control). Instead of the values below, they should match your own app’s key and secret that you obtained previously. You cannot just copy and paste this file. I totally made up these numbers, and they will not work.

.env
1
2
FACEBOOK_KEY=686338067928534
FACEBOOK_SECRET=60b9fe21ab431897eabc0743b39a5406

Facebook Powers: Activate!

Step 1: Create a model to store the authentications

We don’t want to tie authentications to a user in a 1-1 way, since we’d love to allow connecting with other social networks by default. We want a user to have many social networks attached, potentially, so we’ll create a model that appropriately attaches to users.

Create an Authentication model using this command:

Terminal
1
bundle exec rails g model Authentication user_id:integer provider:string uid:string name:string oauth_token:string oauth_expires_at:datetime

This will create a migration for you similar to this:

db/migrate/20131015222934_create_authentications.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CreateAuthentications < ActiveRecord::Migration
  def change
    create_table :authentications do |t|
      t.integer :user_id
      t.string :provider
      t.string :uid
      t.string :name
      t.string :oauth_token
      t.datetime :oauth_expires_at

      t.timestamps
    end
  end
end

You’ll also need to provide a mechanism for Omniauth to save the data it can provide to your DB for your use. After you issue the above command, you should have an app/models/authentications.rb file. Replace that file with these contents:

app/models/authentication.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Authentication < ActiveRecord::Base

  belongs_to :user

  def self.from_omniauth(user, auth)
    where(auth.slice(:provider, :uid)).first_or_initialize.tap do |authentication|
      authentication.user = user
      authentication.provider = auth.provider
      authentication.uid = auth.uid
      authentication.name = auth.info.name
      authentication.oauth_token = auth.credentials.token
      authentication.oauth_expires_at = Time.at(auth.credentials.expires_at)
      authentication.save!
    end
  end

end

Be sure to also add has_many :authentications to your app/models/user.rb file. And don’t forget to run your database migrations here!

Step 2: Create the authentication user interface

Terminal
1
bundle exec rails g controller authentications index

This will create an authentications controller, and a view file for you. We don’t quite want any of what we get out of the package. Copy these file contents into your authentications controller:

app/controllers/authentications_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class AuthenticationsController < ApplicationController

  def index
    @authentications = current_user.authentications if current_user
  end

  def create
    auth = Authentication.from_omniauth(current_user, env["omniauth.auth"])
    flash[:notice] = "Authentication successful."
    redirect_to authentications_url
  end

  def destroy
    @authentication = current_user.authentications.find(params[:id])
    @authentication.destroy
    flash[:notice] = "Successfully destroyed authentication."
    redirect_to authentications_url
  end

end

Place this file into your JavaScript directory. Note that this one won’t work either by just copying and pasting! You need to include your non-sensitive App ID :

app/assets/javascripts/facebook.js
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
// Additional JS functions here
window.fbAsyncInit = function() {
  FB.init({
    appId      : 686338067928534, // App ID
    status     : true, // check login status
    cookie     : true, // enable cookies to allow the server to access the session
    xfbml      : true  // parse XFBML
  });
};

// Load the SDK Asynchronously
(function(d){
   var js, id = 'facebook-jssdk', ref = d.getElementsByTagName('script')[0];
   if (d.getElementById(id)) {return;}
   js = d.createElement('script'); js.id = id; js.async = true;
   js.src = "//connect.facebook.net/en_US/all.js";
   ref.parentNode.insertBefore(js, ref);
}(document));

function fblogin() {
  FB.getLoginStatus(function(response) {
    if(response.status == "connected") {
      location.href =
        '/auth/facebook/callback?' +
        $.param({ signed_request: response.authResponse.signedRequest })
    } else {
     FB.login(function(response) {
      if (response.authResponse) {
          '/auth/facebook/callback?' +
          $.param({ signed_request: response.authResponse.signedRequest })
      }
     })
    }
  })
};

Copy this view file into place, as the bulk of our tutorial actions will happen there. There are HTML comment blocks below— my blog assumes I want a hyphen, when I really do want two dashes. Be sure the file matches this gist.

app/views/authentications/index.html.erb
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
<% if @authentications %>
  <% unless @authentications.empty? %>
    <p><strong>You can sign in to this account using:</strong></p>
    <div class="authentications">
      <% for authentication in @authentications %>
        <div class="authentication">
          <%= image_tag "#{authentication.provider}_32.png", :size => "32x32" %>
          <div class="provider"><%= authentication.provider.titleize %></div>
          <div class="uid"><%= authentication.uid %></div>
          <%= link_to "X", authentication, :confirm => 'Are you sure you want to remove this authentication option?', :method => :delete, :class => "remove" %>
        </div>
      <% end %>
      <div class="clear"></div>
    </div>
  <% end %>
  <p><strong>Add another service to sign in with:</strong></p>
<% else %>
  <p><strong>Sign in through one of these services:</strong></p>
<% end %>

<!—
<a href="/auth/twitter" class="auth_provider">
  <%= image_tag "twitter_64.png", :size => "64x64", :alt => "Twitter" %>
  Twitter
</a>
—>
<% unless @authentications.select{ |a| a.provider == "facebook" }.any? %>
  <%= link_to_function image_tag("facebook_64.png", :size => "64x64", :alt => "Facebook"), 'fblogin()' %>
<% end %>
<!—
<a href="/auth/google_apps" class="auth_provider">
  <%= image_tag "google_64.png", :size => "64x64", :alt => "Google" %>
  Google
</a>
—>

<div class="clear"></div>

Let’s pretty this up a little with some CSS:

app/stylesheets/authentications.css.scss
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
.authentications {
  margin-bottom: 30px;
}

.authentication {
  width: 130px;
  float: left;
  background-color: #EEE;
  border: solid 1px #999;
  padding: 5px 10px;
  -moz-border-radius: 8px;
  -webkit-border-radius: 8px;
  position: relative;
  margin-right: 10px;
}

.authentication .remove {
  text-decoration: none;
  position: absolute;
  top: 3px;
  right: 3px;
  color: #333;
  padding: 2px 4px;
  font-size: 10px;
}

.authentication .remove:hover {
  color: #CCC;
  background-color: #777;
  -moz-border-radius: 6px;
  -webkit-border-radius: 6px;
}

.authentication img {
  float: left;
  margin-right: 10px;
}

.authentication .provider {
  font-weight: bold;
}

.authentication .uid {
  color: #666;
  font-size: 11px;
}

.auth_provider img {
  display: block;
}

.auth_provider {
  float: left;
  text-decoration: none;
  margin-right: 20px;
  text-align: center;
  margin-bottom: 10px;
}

Step 3: Putting it all together

First, the above step created some cruft in your routes that will make you unhappy later. Remove the line that says get “authentications#index” from your routing file. Your route file should contain a trace of the authentications you plan on dealing with. Use this snippet:

config/routes.rb
1
2
3
4
  resources :authentications

  match 'auth/:provider/callback', to: 'authentications#create', via: [:get, :post]
  match 'auth/failure', to: redirect('/'), via: [:get, :post]

Ensure that your Facebook JavaScript is loaded into the asset pipeline:

app/assets/javascripts/application.js
1
//= require facebook

Ensure that your authentications CSS is loaded into the asset pipeline:

app/assets/stylesheets/application.css.scss
1
@import "authentications";

Include a link to social logins to your already logged in accounts:

app/views/layouts/application.html.erb
1
<%= link_to "Social Accounts", authentications_path %>

Download some Facebook icons:

Terminal
1
2
curl -o app/assets/images/facebook_32.png https://www.evernote.com/shard/s1/sh/c3303305-ff8f-4829-a315-9ee91709f479/49b3985624af8fb0cae8e3223333c61a/deep/0/facebook_32.png
cp app/assets/images/facebook_32.png app/assets/images/facebook_64.png

You can use Facebook to let people create new accounts, but that’s left as an exercise to the reader!

Step 4: Boot it up

Issue a command to start your server:

Terminal
1
bundle exec foreman start

Since setting environment variables is important to the application running properly, rails s probably won’t work any longer! Foreman will put us in good position to manage environment variables (and pull user social graphs via Sidekiq later!)

You should be able to click through to the

Acknowledgements:

Many of the file contents are avialable in this gist.

The amazing RailsCasts series provided not one but two pieces of this finished puzzle. Thanks, Ryan!

This StackOverflow post on invalid_credentials in Omniauth-facebook pointed out the need to use a particular omniauth-facebook release. Another StackOverflow post pointed out the need to send a signed_request parameter with the GET to the Facebook SDK. That’s incorporated into the above JavaScript.