Jumpstart Lab Curriculum

Authentication & Authorization

Remote Authentication with OmniAuth

There have been about a dozen popular methods for authenticating Rails applications over the past five years.

As we learn more about constructing web applications there is a greater emphasis on decoupling components. It makes a lot of sense to depend on an external service for our authentication, then that service can serve this application along with many others.

For this section, it’s easiest to understand the concepts by following along and modifying the sample application as you go.

Follow these Setup Instructions to get going with Blogger

Why OmniAuth?

The best application of this concept is the OmniAuth.

It’s popular because it allows you to use multiple third-party services to authenticate, but it is really a pattern for component-based authentication. You could let your users login with their Twitter account, but you could also build your own centralized OmniAuth provider that authenticates all your company’s apps. Maybe you can use the existing LDAP provider to hook into ActiveDirectory or OpenLDAP, or make use of the Google Apps interface.

Better yet, OmniAuth can handle multiple concurrent strategies, so you can offer users multiple ways to authenticate. Your app is just built against the OmniAuth interface, those external components can come and go.

Getting Started with OmniAuth

The first step is to add the dependency to your Gemfile:

1
  gem "omniauth", "~> 0.3.0"

Then run bundle from your terminal.

Rack Middleware

OmniAuth runs as a "Rack Middleware" which means it’s not really a part of our app, it’s a thin layer between our app and the client.

Create the Initializer

To instantiate and control the middleware, we need an initializer. You’d create /config/initializers/omniauth.rb and add the following:

1
2
3
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :twitter, "EZYxQSqP0j35QWqoV0kUg", "IToKT8jdWZEhEH60wFL94HGf4uoGE1SqFUrZUR34M4"
end

What is all that? Twitter, like many API-providing services, wants to track who’s using it. They accomplish this by distributing API accounts.

Specifically, they use the OAuth protocol which requires a consumer key and a consumer secret. If you want to build an application using the Twitter API you’ll need to register and get your own credentials

Accessing the Remote Service

You need to restart your server so the initializer is run and the middleware loaded. The default URL pattern is:

1
http://your.com/auth/provider

Where provider could be twitter, facebook, or any other registered OmniAuth provider. We’ll experiment using Twitter.

In your browser go to http://127.0.0.1:8080/auth/twitter and, after a few seconds, you should see a Twitter login page.

Login to Twitter using any account, then you should see a Routing Error from your application. If you’ve got that, then things are on the right track.

If you get to this point and encounter a 401 Unauthorized message there is more work to do. You’re probably using your own API key and secret. You need to go into the settings on Twitter for your application, and add http://127.0.0.1 as a registered callback domain. Also add http://0.0.0.0 and http://localhost while you’re in there. Now give it a try and you should get the Routing Error.

Handling the Callback

The authentication pattern starts with your app redirecting to the third party authenticator, the third party processes the authentication, then it sends the user back to your application at a callback URL. OmniAuth defaults to listening at /auth/twitter/callback.

You’d handle that callback by adding a route in /config/routes.rb:

1
get '/auth/:provider/callback', to: 'sessions#create'

Your router will attempt to call the create action of the SessionsController when the callback is triggered.

Creating a Sessions Controller

You can generate a controller at the command line and add a create method like this:

1
2
3
4
5
class SessionsController < ApplicationController
  def create
    render text: debug request.env["omniauth.auth"]
  end
end

Calling render :text is a good debugging technique to display plain text as the response. Here you’d see the response body data stored under the omniauth.auth key.

Creating a User Model

Even though we’re using an external service for authentication, we’ll still need to keep track of user objects within our system. Let’s create a model that will be responsible for that data.

As you saw, Twitter gives us a ton of data about the user. What should we store in our database? The minimum expectations for an OmniAuth provider are three things:

  • provider - A string name uniquely identifying the provider service
  • uid - An identifying string uniquely identifying the user within that provider
  • name - Some kind of human-meaningful name for the user

Let’s start with just those three in our model. From your terminal:

Terminal

$
rails generate model User provider:string uid:string name:string

Then update the database with rake db:migrate.

Creating Actual Users

How you create users might vary depending on the application. For our demonstration, we’ll allow anyone to create an account automatically just by logging in with the third party service.

Hop back to the SessionsController. The controller should have as little domain logic as possible, so we’ll proxy the User lookup/creation from the controller down to the model like this:

1
2
3
  def create
    @user = User.find_or_create_by_auth(request.env["omniauth.auth"])
  end

Now the User model is responsible for figuring out what to do with that big hash of data from Twitter. Open the model file and add this method:

1
2
3
4
5
6
7
8
  def self.find_or_create_by_auth(auth_data)
    user = self.find_or_create_by_provider_and_uid(auth_data["provider"], auth_data["uid"])
    if user.name != auth_data["user_info"]["name"]
      user.name = auth_data["user_info"]["name"]
      user.save
    end
    return user
  end

To walk through that step by step…

  • Look in the users table for a record with this provider and uid combination. If it’s found, you’ll get it back. If it’s not found, a new record will be created and returned
  • Compare the user’s name and the name in the auth data. If they’re different, either this is a new user and we want to store the name or they’ve changed their name on the external service and it should be updated here. Then save it.
  • Either way, return the user

Now, back to SessionsController, we need to save the logged in user’s id in the session. And let’s add a redirect action to send them to the articles_path after login:

1
2
3
4
5
  def create
    @user = User.find_or_create_by_auth(request.env["omniauth.auth"])
    session[:user_id] = @user.id
    redirect_to articles_path, notice: "Logged in as #{@user.name}"
  end

Now visit /auth/twitter and you should eventually be redirected to the Articles listing.

UI for Login/Logout

That’s exciting, but now we need links for login/logout that don’t require manually manipulating URLs. Anything like login/logout that you want visible on every page goes in the layout.

Open /app/views/layouts/application.html.erb and you’ll see the framing for all our view templates. Let’s add in the following:

1
2
3
4
5
6
7
8
  <div id="account">
    <% if current_user %>
      <span>Welcome, <%= current_user.name %></span>
      <%= link_to "logout", logout_path, id: "logout" %>
    <% else %>
      <%= link_to "login", login_path, id: "login" %>
    <% end %>
  </div>

If you refresh your browser that will crash for several reasons.

Accessing the Current User

It’s a convention that Rails authentication systems provide a current_user method to access the user.

Let’s create that in our ApplicationController with these steps:

  • Underneath the protect_from_forgery line, add this:

    ruby helper_method :current_user

  • Just before the closing end of the class, add this:

    ruby private def current_user @current_user ||= User.find(session[:user_id]) if session[:user_id] end

By defining the current_user method as private in ApplicationController, that method will be available to all our controllers because they inherit from ApplicationController.

In addition, the helper_method line makes the method available to all our views. Now we can access current_user from any controller and any view!

Progress Check

Refresh the page in your browser and you’ll move on to the next error:

1
undefined local variable or method `login_path'.

Convenience Routes

Just because we’re following the REST convention doesn’t mean we can’t also create our own named routes. The view snippet we wrote is attempting to link to login_path and logout_path, but our application doesn’t yet know about those routes.

Open /config/routes.rb and add two custom routes:

1
2
  get "/login" => redirect("/auth/twitter"), as: :login
  get "/logout" => "sessions#destroy", as: :logout

The first line creates a path named login which redirects to the static address /auth/twitter which will be intercepted by the OmniAuth middleware. The second line creates a logout path which will call the destroy action of our SessionsController.

With those in place, refresh your browser and it should load without error.

Implementing Logout

Our login works great, but we can’t logout! When you click the logout link it’s attempting to call the destroy action of SessionsController. Let’s implement that.

  • Open SessionsController
  • Add a destroy method
  • In the method, erase the session with:

    ruby session[:user_id] = nil

  • Redirect to the root_path with the notice "Goodbye!"

  • Define a root_path in your router like this:

    ruby root to: "article#index"

Wrapup

At that point, your login/logout system should be working!

That’s just the beginning with OmniAuth. Now, you could choose to add other providers by adding API keys to the initializer and properly handling the different routes.

You might try out some of these:

References

Feedback

Have Feedback?

Did you find an error? Something confusing? We'd love your help:

Thanks!