Ruby
Article

Rails Authentication with OAuth 2.0 and OmniAuth

By Ilya Bodrov-Krukowski

Authentication in Rails

This is the third article in the Authentication with Rails series. We’ve build a classic login/password authentication systems with features like remember me, reset password, confirm e-mail, and the like.

Today we are going to talk about authentication via social networks with the help of the OAuth 2 protocol. We will discuss OmniAuth and four of its strategies: Twitter, Facebook, Google+, and LinkedIn, allowing users to log in with any network they like. I will instruct you how to integrate each strategy, set it up, and handle errors.

Source code for this article is available on GitHub.

A working demo can be found at sitepoint-oauth2.herokuapp.com.

OAuth 2 and OmniAuth

OAuth 2 is an authorization protocol that enables a third-party applications to obtain limited access to an HTTP service. One of the main aspects of this protocol is the access token that is issued to the application. This token is used by the app to perform various actions on the user’s behalf. However, it can’t perform something that was not approved (for example, the user may only allow an app to fetch information about friends, but not post on the user’s wall).

This is cool because, under no circumstances should a third-party app access a user’s password. It only gets a special token that expires after some time (typically, users may also revoke access manually, as well) and may only be used to perform a list of approved tasks, called a scope.

How is a third-party app identified? Well, when talking about social networks, an app has to registered first. The developer provides the app’s URL, name, contact data, and other information that will be visible to the user. The authorization provider (Twitter or Facebook) then issues a key pair that uniquely identifies the app for the social network/provider.

Here is the simplistic overview of what happens when a user visits the app and tries to authenticate via a social network:

  • User clicks “Login” link
  • User is redirected to the social network’s website. The app’s data (client_id) are sent along for identification
  • User sees the app’s details (like name, logo, description, etc.) and which actions it would like to perform on his behalf (the scope). Think of it like you coming to the user and saying: “Hey, my name is Jack and I want to get the list of all your friends! If you agree, I’ll show you interesting statistics about them”
  • If the user does not trust this app, they just cancel the authorization
  • If the user trusts the app, the authorization is approved and the user is redirected back to the app (via callback URL). Information about the authenticated user and a generated token (sometimes with a secret key) is sent along.

There are numerous of services that support OAuth 2 authentication, so to standardize the process of creating authentication solutions OmniAuth was created by Intridea, Inc. It supports anything from OAuth 2 to LDAP. In this article, we will focus on omniauth-oauth2, an abstract OAuth 2 strategy. Basically it is used as a building block to easily craft authentication strategies on top of it. Here is the huge list of all strategies available for OmniAuth (it also includes those which are not related to OAuth 2).

Having a standard approach for strategies is great because you can integrate as many as you want without issue. Still, you have to remember that some social networks may return a different data about the authenticating user, so testing how your app works is absolutely necessary (I will show you some examples on this.)

If you have never integrated OAuth 2 into an app, this all might seem complicated, but don’t worry, soon it will become very clear! It is nice to learn by example, so let’s dive into the code and create something fancy.

Preparing the Demo App

Okay, create a simple app allowing users to authenticate via one of the presented social networks. I’m calling mine SocialFreak:

$ rails new SocialFreak -T

Rails 4 will be used for this demo, but Rails 3 should work just as well.

Hook up Bootstrap to style the app (optional):

Gemfile

[...]
gem 'bootstrap-sass'
[...]

and run

$ bundle install

Import the necessary files:

stylesheets/application.css.scss

@import "bootstrap-sprockets";
@import "bootstrap";
@import 'bootstrap/theme';

Modify the layout:

layouts/application.html.erb

[...]
<nav class="navbar navbar-inverse">
  <div class="container">
    <div class="navbar-header">
      <%= link_to 'Social Freak', root_path, class: 'navbar-brand' %>
    </div>
    <div id="navbar">
      <ul class="nav navbar-nav">

      </ul>
    </div>
  </div>
</nav>

<div class="container">
  <% flash.each do |key, value| %>
    <div class="alert alert-<%= key %>">
      <%= value %>
    </div>
  <% end %>

  <%= yield %>
</div>
[...]

This sets up main menu (with no items yet) and an area to render flash messages.

Create the root page:

config/routes.rb

[...]
root to: 'pages#index'
[...]

pages_controller.rb

class PagesController < ApplicationController
  def index

  end
end

views/pages/html.erb

<div class="jumbotron">
  <h1>Welcome!</h1>
  <p>Authenticate via one of the social networks to get started.</p>
</div>

Authentication Via Twitter

Adding a Strategy

Let’s use the omniauth-twitter gem created by Arun Agrawal. It is one of the numerous strategies for OmniAuth. Drop it in the Gemfile:

Gemfile

[...]
gem 'omniauth-twitter'
[...]

and run

$ bundle install

Configuration for all the OmniAuth strategies lives in the omniauth.rb initializer file, so create it now:

config/initializers/omniauth.rb

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET']
end

Registering a new strategy (provider) is a matter of providing a key pair to identify the app to the provider. Of course, we need to obtain those two keys, so do the following:

  • Navigate to apps.twitter.com
  • Click “Create new app”
  • Fill in the form. For the callback URL, provide your site’s address plus “/auth/twitter/callback”. If you are on a local machine, provide “http://localhost:3000/auth/twitter/callback”. We will discuss this callback URL shortly.
  • Click “Create”.
  • You will be redirected to the app’s information page on Twitter. Navigate to the “Keys and Access Tokens” tab.
  • Copy the Consumer Key and Consumer Secret and paste them into the initializer file.
  • You may also browse other tabs, but for this demo we won’t change anything else. For example, inside “Permissions” you can set which actions the app will be able to perform after a user has authenticated. If you needed, for example, to be able to post tweets on behalf of the user, you’d have to change permissions accordingly. This controls the aforementioned “scope” of the authorization.

Please note that you may further set up this strategy as described here. For this demo, default values will work just fine.

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.

With this knowledge, let’s set up routes accordingly:

config/routes.rb

[...]
get '/auth/:provider/callback', to: 'sessions#create'
[...]

and add the first link to our main menu:

layouts/application.html.erb

[...]
<ul class="nav navbar-nav">
  <li><%= link_to 'Twitter', '/auth/twitter' %></li>
</ul>
[...]

Note that the /auth/twitter route is provided by the strategy and redirects to the Twitter login page.

Authentication Hash

The create method inside the SessionsController will parse the user data, save it, into the database, and perform sign in to the app. However, what does the user data look like? That’s easy to check:

sessions_controller.rb

class SessionsController < ApplicationController
  def create
    render text: request.env['omniauth.auth'].to_yaml
  end
end

request.env['omniauth.auth'] contains the Authentication Hash with all the data about a user.

Reload the server and try to authenticate via Twitter. You will see something like this. Note that, apart from your name, location, avatar, and other basic info, there is also some special data like followers and tweets count. Of course, this data will vary between providers (and some social networks send only basic information, by default).

Saving User Data

Now that you know what the authentication hash looks like and what data can be fetched, it’s time to decide which information to store. Since we want to craft multi-provider authentication, saving something like tweets count is not the top priority (however, you could just serialize and save the “extra” part of user’s authentication hash in a separate field).

For this demo let’s store the following:

  • Provider’s name. This is absolutely necessary because we will have multiple providers.
  • User’s unique identifier. This identifier is generated by a social network and may contain various symbols. Some networks use only numbers, while others use letters as well. Combination of provider’s name and uID will uniquely identify user inside our app.
  • User’s full name. Some networks also provide name and surname separately (and some, like Twitter, also separate name and nickname) but we don’t need such complexity.
  • User’s location. Not all networks provide this, but let’s research which ones do and in what format.
  • User’s avatar URL. We will display user’s avatar in the main menu, so it has to be pretty small. Fortunately, most social networks allow you to choose from one of a several available sizes. Use the image_size option for Twitter to control this (the default is 48x48px which suites us perfectly). Also, note that, by default, avatar URLs have http as the protocol. If you want everything on your page to use https, many social networks allow this. For example, with Twitter you have to set secure_image_url to true.
  • User’s profile URL. Every social network provides this. However, in many cases, the urls key has a nested hash like so:


:urls => {
:Website => 'http://example.com',
:Twitter => "https://twitter.com/xxx"
}

We are not storing the user’s token because we won’t actually perform any actions on their behalf. If you need this, create the token and secret fields with a string data type (some social networks provide secret as well). Use those when sending API requests to the service.

Okay, now we can generate the appropriate migration:

$ rails g model User provider:string uid:string name:string location:string image_url:string url:string

Open migration’s file and modify it like this:

xxx_create_users.rb

[...]
t.string :provider, null: false
t.string :uid, null: false
add_index :users, :provider
add_index :users, :uid
add_index :users, [:provider, :uid], unique: true
[...]

provider and uid cannot be null and should be indexed. add_index :users, [:provider, :uid], unique: true adds a clustered index on those two fields and makes sure that their combination is unique.

Apply the migration:

$ rake db:migrate

Now re-write the create action

sessions_controller.rb

[...]
def create
  begin
    @user = User.from_omniauth(request.env['omniauth.auth'])
    session[:user_id] = @user.id
    flash[:success] = "Welcome, #{@user.name}!"
  rescue
    flash[:warning] = "There was an error while trying to authenticate you..."
  end
  redirect_to root_path
end
[...]

from_omniauth is a yet non-existent method that will parse the authentication hash and return the user record. Next, just save the user’s id inside the sessions and redirect to the main page.

It is time to take care of the from_omniauth class method:

user.rb

[...]
class << self
  def from_omniauth(auth_hash)
    user = find_or_create_by(uid: auth_hash['uid'], provider: auth_hash['provider'])
    user.name = auth_hash['info']['name']
    user.location = auth_hash['info']['location']
    user.image_url = auth_hash['info']['image']
    user.url = auth_hash['info']['urls']['Twitter']
    user.save!
    user
  end
end
[...]

find_or_create_by ensures that we are not creating the same user multiple times. The method stores all the required data, saves the user, and returns it. If you are interested, the user’s token can normally be accessed as auth_hash['credentials']['token'] (auth_hash['credentials']['secret'] for secret).

Current User and Logging Out

We need a way to find out whether a user is logged in or not. current_user is a method that, by convention, either returns a user record or nil:

application_controller.rb

[...]
private

def current_user
  @current_user ||= User.find_by(id: session[:user_id])
end

helper_method :current_user
[...]

helper_method :current_user ensures that it can be called from the views, as well.

Great, now the user can log in, but can’t log out. Add a logout link:

layouts/application.html.erb

[...]
<nav class="navbar navbar-inverse">
  <div class="container">
    <div class="navbar-header">
      <%= link_to 'Social Freak', root_path, class: 'navbar-brand' %>
    </div>
    <div id="navbar">
      <% if current_user %>
        <ul class="nav navbar-nav pull-right">
          <li><%= image_tag current_user.image_url, alt: current_user.name %></li>
          <li><%= link_to 'Log Out', logout_path, method: :delete %></li>
        </ul>
      <% else %>
        <ul class="nav navbar-nav">
          <li><%= link_to 'Twitter', '/auth/twitter' %></li>
        </ul>
      <% end %>
    </div>
  </div>
</nav>
[...]

Note that I’ve also added the user’s avatar to the main menu.

The corresponding route:

config/routes.rb

[...]
delete '/logout', to: 'sessions#destroy'
[...]

and the controller action:

sessions_controller.rb

[...]
def destroy
  if current_user
    session.delete(:user_id)
    flash[:success] = 'See you!'
  end
  redirect_to root_path
end
[...]

Lastly, modify the main page to display information about the currently logged in user:

views/pages/index.html.erb

<% if current_user %>
  <div class="page-header">
    <h1>Here is some info about you...</h1>
  </div>
  <div class="panel panel-default">
    <div class="panel-body">
      <ul>
        <li><strong>Name:</strong> <%= current_user.name %></li>
        <li><strong>Provider:</strong> <%= current_user.provider %></li>
        <li><strong>uID:</strong> <%= current_user.uid %></li>
        <li><strong>Location:</strong> <%= current_user.location %></li>
        <li><strong>Avatar URL:</strong> <%= current_user.image_url %></li>
        <li><strong>URL:</strong> <%= current_user.url %></li>
      </ul>
    </div>
  </div>
<% else %>
  <%= render 'welcome' %>
<% end %>

I’ve moved the welcoming message to a separate partial:

views/pages/_welcome.html.erb

<div class="jumbotron">
  <h1>Welcome!</h1>
  <p>Authenticate via one of the social networks to get started.</p>
</div>

That was a lot to cover, but now we’ve set up the base and adding new providers will be really easy (mostly)!

Authenticating via Facebook

We are going to use the omniauth-facebook gem by Mark Dodwell.

Drop it into the Gemfile

Gemfile

[...]
gem 'omniauth-facebook'
[...]

and run

$ bundle install

The idea is the same: We have to set up the new provider and make sure that the authentication hash is parsed correctly.

config/initializers/omniauth.rb

[...]
provider :facebook, ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET']
[...]

To get Facebook key and secret:

  • Navigate to developers.facebook.com.
  • Click “Add new app” under “My Apps” menu item.
  • Click “Website” in the dialog.
  • Click “Skip and Create ID”.
  • Enter a name for your app and choose a category, click “Create”.
  • You will be redirected to the app’s page. Click “Show” next to the “App Secret” and enter your password to reveal the key. Copy and paste those keys into your initializer file.

Don’t leave this page yet. You have to do some set up to make the app active:

  • Open “Settings” section.
  • Click “Add Platform” and choose “Website”.
  • Fill in “Site URL” (“http://localhost:3000” for local machine) and “App Domains” (must be derived from the Site URL or Mobile Site URL).
  • Fill in “Contact E-mail” (it is required to make app active) and click “Save Changes”
  • Navigate to the “Status & Review” section and set the “Do you want to make this app and all its live features available to the general public?” switch to “Yes”.

This is the minimal set up – there is much more you can do with your app, so feel free to visit other sections as well.

For the Facebook provider we also have to do some set up in the Rails app:

config/initializers/omniauth.rb

[...]
provider :facebook, ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET'],
  scope: 'public_profile', info_fields: 'id,name,link'
[...]

scope is the list of permissions that we are requesting from the user upon authorization. public_profile means that only basic info will be available to our app and no actions on user’s behalf may be performed. There are loads of permissions that may be requested from the user, including Extended ones that actually allow you to get access to sensitive data and do some actions on user’s behalf.

info_fields is the list of fields that we want to be included in the Authentication Hash. Please note that some fields will always be present. Here is the list of other fields that are available.

Like Twitter, Facebook supports various avatar sizes (image_size, the default is 50×50 that suits us well) and supports https to access the avatar (secure_image_url). Here are all the settings that you may utilize.

Great, now add a new link to the main menu:

layouts/application.html.erb

[...]
<li><%= link_to 'Facebook', '/auth/facebook' %></li>
[...]

It is time to tweak the from_omniauth method (here is an example of the authentication hash). Actually, only one line has to be modified:

models/user.rb

[...]
user.url = auth_hash['info']['urls'][user.provider.capitalize]
[...]

Now you may restart the server and check it out!

Authenticating via Google+

We will employ omniauth-google-oauth2 gem by Josh Ellithorpe.

Drop it in the Gemfile:

Gemfile

[...]
gem "omniauth-google-oauth2"
[...]

and run

$ bundle install

You know the rules. Add the new provider to the initializer file:

config/initializers/omniauth.rb

[...]
provider :google_oauth2, ENV["GOOGLE_CLIENT_ID"], ENV["GOOGLE_SECRET"]
[...]

To obtain the Google key and secret:

  • Navigate to console.developers.google.com.
  • Click “Create Project” and give it a name.
  • Open “APIs” section from the right-side menu and make sure Google+ API is enabled.
  • Open “Consent Screen” and fill in “Product Name” (and other fields if you wish).
  • Open “Credentials” and click “Create new Client ID”.
  • Choose “Web Application”.
  • Enter your app’s URL in the “Authorized JavaScript origins” (“http://localhost:3000” for local machine).
  • Enter your app’s URL plus “/auth/google_oauth2/callback” in “Authorized redirect URIs”.

Copy the Client ID and Client Secret and paste it into the initializer file.

Now some settings in our Rails app:

config/initializers/omniauth.rb

[...]
provider :google_oauth2, ENV["GOOGLE_CLIENT_ID"], ENV["GOOGLE_SECRET"],
  scope: 'profile', image_aspect_ratio: 'square', image_size: 48, access_type: 'online'
[...]

scope: 'profile' means that we only want to receive basic user info. Google provides a whole bunch of APIs so visit Playground to see a full list of available scopes.

image_aspect_ratio: 'square' means that we want to get user’s avatar with equal width and height (original is the default value).

image_size: 48 means that user avatar will have 48px size for width and height.

access_type: 'online' means that we do not want to receive a refresh token. This means that when a token that was given for us by Google expires, we won’t be able to refresh it (using this special refresh token) so the user will have to log in once again. Offline access type is useful when you want to perform some periodic actions on user’s behalf without asking to constantly log in. Even if user’s token expires, your program will be able to refresh it automatically. This is a bit beyond the scope of this post.

There are some other configuration options that you may want to utilize.

Return to the from_omniauth method and have a look at this line:

models/user.rb

[...]
user.url = auth_hash['info']['urls'][user.provider.capitalize]
[...]

For Google+, the provider’s name will be “google_oauth2”, but a matching key won’t be found inside the urls nested hash. It will be something like:

:urls => {
  :Google => "https://plus.google.com/xxx"
}

We could add a basic conditional statement here, but it’s better to rename our provider, like this:

config/initializers/omniauth.rb

[...]
provider :google_oauth2, ENV["GOOGLE_CLIENT_ID"], ENV["GOOGLE_SECRET"],
         scope: 'profile', image_aspect_ratio: 'square', image_size: 48, access_type: 'online', name: 'google'
[...]

Don’t forget to change the Callback URL in the Google Developer Console to “/auth/google/callback”!

Now add another link to the main menu:

layouts/application.html.erb

[...]
<li><%= link_to 'Google+', '/auth/google' %></li>
[...]

Reload the server and try to authenticate!

Authentication with LinkedIn

This will be the last social network for today and will use omniauth-linkedin-oauth2 gem by Decio Ferreira:

Gemfile

[...]
gem 'omniauth-linkedin-oauth2'
[...]

Don’t forget to run

$ bundle install

Register a new provider:

[...]
provider :linkedin, ENV['LINKEDIN_KEY'], ENV['LINKEDIN_SECRET']
[...]

To obtain LinkedIn key and secret:

  • Navigate to linkedin.com/secure/developer.
  • Click “Add new application”.
  • Fill in the required fields. This form is much bigger (and a bit buggy) than you’ve seen when registering apps for other social networks, so be patient.
  • Fill in “OAuth 2.0 Redirect URLs” with a URL in the format “/auth/linkedin/callback”.
  • After submitting the form, open your app’s page and copy Consumer key and Consumer secret into your initializer file.

Add some configuration options:

[...]
provider :linkedin, ENV['LINKEDIN_KEY'], ENV['LINKEDIN_SECRET'],
  scope: 'r_basicprofile',
  fields: ['id', 'first-name', 'last-name', 'location', 'picture-url', 'public-profile-url']
[...]

scope: 'r_basicprofile' means that we only want to get basic user data.

fields contains an array of all the fields that should be returned in the Authentication Hash. The list of default fields can be found here.

Add another link to the main menu:

layouts/application.html.erb

[...]
<li><%= link_to 'LinkedIn', '/auth/linkedin' %></li>
[...]

This is where things start getting slightly more complicated, because LinkedIn’s authentication hash is a bit different from the ones that we’ve seen up to now:

  • location is a nested hash with country’s code and name.
  • urls is also a nested hash, but there is no LinkedIn key. Instead, there is a public_profile key and probably a bunch of others (storing personal web site and things like that).

This introduces some complexity to the parsing method:

models/user.rb

class << self
  def from_omniauth(auth_hash)
    user = find_or_create_by(uid: auth_hash['uid'], provider: auth_hash['provider'])
    user.name = auth_hash['info']['name']
    user.location = get_social_location_for user.provider, auth_hash['info']['location']
    user.image_url = auth_hash['info']['image']
    user.url = get_social_url_for user.provider, auth_hash['info']['urls']
    user.save!
    user
  end

  private

  def get_social_location_for(provider, location_hash)
    case provider
      when 'linkedin'
        location_hash['name']
      else
        location_hash
    end
  end

  def get_social_url_for(provider, urls_hash)
    case provider
      when 'linkedin'
        urls_hash['public_profile']
      else
        urls_hash[provider.capitalize]
    end
  end
end

I’ve used location_hash['name'] to get the name of the country where user resides and urls_hash['public_profile'] to get only user’s profile URL. Of course, we may use an if conditional instead, but some time in the future you may add more providers and who knows which inconsistencies that will bring. Feel free to refactor this code further.

Unfortunately, LinkedIn does not allow to specify avatar’s dimensions (at least, I have not found a way – if you know one, please share it with me :)), so add this simple style to your stylesheet:

stylesheets/application.scss

[...]
nav {
  img {
    max-width: 48px;
  }
}

Now you may reload your server and log in via LinkedIn!

Rescuing from Errors

The last thing that we are going to discuss today is rescuing from errors that occurs during authentication phase. You may recall that inside the create method we have:

sessions_controller.rb

[...]
begin
  @user = User.from_omniauth(request.env['omniauth.auth'])
  session[:user_id] = @user.id
  flash[:success] = "Welcome, #{@user.name}!"
rescue
 flash[:warning] = "There was an error while trying to authenticate you..."
end
[...]

This indeed rescues any error that was raised inside the from_omniauth method. However, this does not protect us from the errors that happened during the authentication. For example, if you disable cookies and try to authenticate via one of the social networks, you’ll get a SessionExpired error.

Rescuing such errors should be done inside the initializer file:

config/initializers/omniauth.rb

[...]
OmniAuth.config.on_failure = Proc.new do |env|
  SessionsController.action(:auth_failure).call(env)
end

We simply call the auth_failure action inside the SessionsController. It could look like this:

sessions_controller.rb

[...]
def auth_failure
  redirect_to root_path
end
[...]

Here is another (uglier) possible option:

config/initializers/omniauth.rb

[...]
OmniAuth.config.on_failure do |env|
  error_type = env['omniauth.error.type']
  new_path = "#{env['SCRIPT_NAME']}#{OmniAuth.config.path_prefix}/failure?message=#{error_type}"
  ,[301, {'Location' => new_path, 'Content-Type' => 'text/html'}, []]
end

This will perform a redirect to “/failure?message=xxx”. Don’t forget to set up the corresponding route and controller action to handle the request.

Conclusion

We’ve created a multi-provider authentication system that may be further expanded any way you like. Hopefully you’ve enjoyed this post! Have you ever integrated authentication via social networks in your projects? Share your experience and don’t hesitate to post your questions. If you’d like me to cover a specific topic, please send an e-mail, I will respond as soon as possible.

Happy coding and stay social!


Comments
Rob_Nichols1

It is important to point out that using find_or_create_by in the User.from_omniauth method means that you will have no control over who has access to your application. That is, anyone with a valid twitter account will be able to generate a user object.

For many applications, that may be the desired behaviour, but there are many other instances where this is not the case. I do not think this should be the default behaviour described in this sort of tutorial. It should be described as an alternative. That is, you should describe the least dangerous configuration first and foremost, and then show alternative options.

bodrovis

I can't fully agree on this, because the whole point of the app is to authenticate anyone and I do not consider it "dangerous", this is just how the app works. We can say the same about traditional login-password authentication - anyone with a valid e-mail can authenticate. We understand, that if you need to restrict access somehow, additional steps have to be taken.

Of course, there may be times when you want to restrict access to a list of users (using invites, maybe), but I do not really think that readers need some additional explanation that with the current setup everyone can authenticate. Still, your concern is understandable and thank you for the feedback. I hope you find another piece of the article acceptable smile

bodrovis

I am just saying that it makes more sense to present solutions (quick and easy solutions if possible) to the common problems, not the safest solutions. Of course, if my solution presented some serious vulnerability I would pinpoint it or rather stay away from it, but that's not the case here.

Rob_Nichols1

The description of your app is "a simple app allowing users to authenticate via one of the presented social networks". However, it is both doing this and allowing user to effectively register via the same mechanism. Where in your piece does it state that "anyone with a valid e-mail can authenticate".

You are conflating authentication with account creation in your example. That's fine, but your article should at least clearly state that.

bodrovis

Nowhere because it does not.

We can say the same about traditional login-password authentication - anyone with a valid e-mail can authenticate.

I did not mean to say that this app allows authenticating via e-mail. I just mean that allowing any user to authenticate (register) via his social network profile (what was done in this app) presents no more security risks that allowing anyone with a valid e-mail to authenticate (register) in some other app.

Yeah, maybe I should have pinpoint the fact that creating an account means authenticating as well, but this is clearly seen from the controller's code. Will keep that thing in mind.

felipecerda

Hi there. First of all, thanks for this very useful post.
I have a question about getting the keys from the providers. For now, I'll be creating it with the localhost:3000 URL as shown in this guide, but what do I have to do once I decide to deploy my app into production?

Thanks!

bodrovis

I've already answered by e-mail, however others might have this question as well: yes, you'll have to do this smile

I'm monitoring discussions for my articles, so you don't have to duplicate questions by e-mail. Cheers!

mynja

Actually, I'm looking for a recommendation for this + access control lists as described. Eg Given users who authenticate, some users can see some things some users can see others. If a user authenticates and they have a role, they gain access to the views defined in said role.

This post is awesome, and exactly what I needed up to that point smile I want to prevent the case of 'anyone getting access to everything' and can't seem to find a good answer.

bodrovis

Thank you! Well, here is my article on CanCan, an authorization solution for Rails http://www.sitepoint.com/cancancan-rails-authorization-dance/ I am also going to cover Pundit in some time. smile

Lavkesh_Agrawal

Hi i am getting the error when google+ callback occurs

Type Error no implicit conversion of String into Integer

Please help me to out this error.Thannks

Lavkesh_Agrawal

Sorry Solved this.Browser shows above error but console was showing that Google+ Api is not enable for your project. So i enabled that one.And Working fine.Thanks for simple and nice article.

bodrovis

Yeah, that was a crucial step. Thank you!

jrpetersjr

I was just wondering, what would I have to change in this to allow the users in my app to be able to sign in sign in/link all of the social networks to my app? This is so that when they press the share button on a post they will be able to share it to all of their social networks at one time

bodrovis

Well, that would be a bit more complex setup and unfortunately you'll have to experiment yourself. The idea is to allow multiple authentications per user and just check that if he already logged in and logging in via another social network, just update his data in the appropriate field.

Padmahas

Hey, thank you for this nice tutorial. Now I've completed only up-to Twitter integration, and struck with a problem.

I run rails server inside Virtual Box with Ubuntu server guest. Hence to access rails app from host, I force rails server with this command "rails s -p 80 -b 192.168.x.xx", So that in host browser entering just IP address will open my rails app and its working fine.
The problem is
In my Twitter app (https://apps.twitter.com/) I have given Website as "http://192.168.x.xx" and Callback URL as "http://192.168.x.xx/auth/twitter/callback", but when I click twitter link from my app, Its showing error "OAuth::Unauthorized". After I googled this issue, I found from StackOverflow that it happens when callback URL is wrongly specified. In this tutorial it is suggested to use "http://localhost:3000" which is not possible in my situation.

Any ideas?
Thank you.

bodrovis

Hi! What do these "x" stand for?

Padmahas

Hello bodrovis, I meant to show it was a dynamic IP and it keeps changing. "x" is a variable. Some times its 192.168.1.15, 192.168.1.18, 192.168.1.14 etc and it keeps changing each time I start Virtualbox. But I made sure that the current generated IP is same as the IP I have given in the twitter. And today I've to change the IP in twitter according to newly generated IP inside Ubuntu guest. What else I am missing?

Thank you.

bodrovis

Wait, if you are developing on your Ubuntu machine and interacting with your app from the same machine, you'll have to provide 127.0.0.1 as an address.

Padmahas

Hey sorry, the problem was with this line "provider :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET']".

As you have stated to copy paste Twitter key and secret, I did exactly same. But later found out that ENV[...] will search for environment variable set in the system. I found the solution after integrating Facebook login, with the error fb_id is not found. The solution was to either set environment variable for TWITTER_KEY and "T..._SECRET" or remove them and copy paste the key and secret as a string, withing double quotes ("sdfjk23424......."). I choose string method.

How ever now I have new problems now.

  1. Using Twitter I can login but I can't logout. For some reason delete request is not being processed. I'm sure of integrating all code to app controller and user controller. Still working on it.

  2. And Facebook authentication happened up-to accepting app permissions. But soon after I accept it is showing me the error " :
    {"error":{"message":"Error validating client secret.","type":"OAuthException","code":1,"fbtrace_id":"FNsnm7HwnE3"}}".
    Still digging on it.

  3. Google+ requires some thing other than basic IP addresses (192.168..) like http://localhost. So its not going to happen anytime soon. I have to map my rails server to no-ip free domain name.

  4. LinkedIn yet have to check. Anyways thank you for your support. I'm glad if you know solution to any of the above.

bodrovis

Yeah, I used ENV to hide my keys.

  1. If you use HTTP DELETE, then your link should be something like <%= link_to 'logout', '/logout', method: :delete %>. jquery_ujs adapter should be present on the page for this work, JS should be enabled in the browser.
  2. Have no idea what this means, maybe google can help :smile:
  3. Hrm, that's interesting, I believe there should be away to test things our using private IP
  4. LinkedIn is the most painful I believe, so good luck :smile:
Mittineague

I have seen discussions pf problems with Facebook not sending an email address unless the account had permissions set to allow it.

Might that be the issue here?

Padmahas

No Mittineageue the problem was, while pasting I had left an inverted comma. So rails treated the app id as wrong. Now I can login with both FB and Twitter.

Padmahas

Hey bodrovis, are you sure the logout does work exactly as expected? Because these, people, have same problem. But I'm one step near to solving it. I changed the code to
if current_user
session.delete(:user_id) if session[:user_id]
# flash[:success] = 'See you!'
redirect_to root_url
end

by putting redirect_to root_url inside if condition. It showed missing template and I created destroy.html.erb. And I was actually logged out. But when I visit the root_path again (not by clicking back button), I was auto logged in. Whats happening?

Padmahas

I've pasted my code here.

bodrovis

Actually I'd love to see the full app to test it out :smile:

szmagyar

Hey Ilya! Nice tut as always. I read/watched all the tuts I could find (including yours, railscats, etc.), but I'm still confused a bit. Pls help me out here. I have a rails app with devise authentication. What I'd like to do is keeping the only devise authentication, but when a user is already signed in w/ devise he/she could connect his social media profiles (twitter, linkedin, angellist) to his basic app profile. Example pic from another app attached: 1. On profile edit page user can connect his/her social media profiles. 2. Once those are connected to the in-app profile the logo will appear on user's show page, so when some other user clicks on his/her profile can go the given user's linkedin/twitter page (on twitter/ on linkedin). 3. If there is no profile pic for the user in my app yet then it will be fetched from twitter and integrated.

My questions:
- What kinda gems are necessary? I will use twitter gem and linkedin gem to make the code shorter, but do I need omniauth gems in this case?
- What kinda api should I use here? I guess which one I need for updating the profile pic based on twitter (GET account/verify_credentials), but I don't know what to use for redirecting the other users to the user's twitter page.
- I know I need to authenticate the user by twitter/linkedin to access their twitter/linkedin info, but in my case does it have to do anything w/ devise or can be totally separated? For instance I use :authenticate_user! in twitter's controller so if a user is logged in to my app then can authorize his/her twitter. So do I need omniauth gem here?

Sry if my questions are not crsytal clear, I couldn't create a mental picture yet how this will come together.

bodrovis

Good day!

  1. You don't need any gems apart from omniauth-twitter, omniauth-linkedin and similar. OAuth providers always return a pretty nice hash of various info, including user's name and avatar. Therefore, you can fetch it from there. Twitter gem is used to perform more advanced stuff like fetching friends, posting tweets on user's behalf etc.
  2. No API is needed, since authentication hash returned by the OAuth provider has all the necessary data
  3. Devise does support other omniauth, but I am not really sure if it is needed here. On the first iteration I'd write everything myself.

Hopefully this helps.

szmagyar

Thanks Ilya! I will try it and will let you know the result!

szmagyar

Ilya, could you tell me, how I can avoid create a session here? All the tuts I found created a new session for twitter, but I guess I don't have to create any. I have to do something with the callback though, but I am not sure what.

bodrovis

Could you contact me directly so that we discuss this issue? http://www.radiant-wind.com/ some channels are listed here.

szmagyar

Sure Ilya. I've just sent a mail.

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Ruby, once a week, for free.