Clean OAuth for Rails: An Object-Oriented Approach

Note:This tutorial is intended for programmers who have implemented OAuth for Rails before and are looking for a better way to do so. It is not a beginner's tutorial. It assumes the reader already has knowledge of the OAuth gems and how to integrate them into a Rails app.

While Ruby's OAuth gems go a long way towards making OAuth implementation with Rails a pain-free process, the standard method of implementation (see here and here) rapidly breaks down when used in a full-scale production app:

  1. It uses one model, User, to combine both OAuth logic and validations with "Standard User" (i.e, a non-OAuth user, created through a standard sign up form) logic and validations. It doesn't take long for clashes to begin. For example, your Standard User must enter an email address and password, but Twitter OAuth provides neither, which causes those validations to fail. The only way out is with messy conditionals.

  2. Logging in with an OAuth account for the first time automatically creates a new user record, providing no way for a Standard User to later integrate an OAuth account into an existing user account.

  3. It provides no easy way to link multiple OAuth accounts (Facebook and Twitter and LinkedIn and Github) with a single user.

  4. The OAuth gems try to standardize the hash of data returned by the various platforms (Facebook, Twitter etc), but they are still not exactly the same. This means that separate, long, almost-identical-but-not-exactly-identical methods need to be added to your user model to deal with each type of OAuth platform.

  5. There is no clean way to trigger different types of callbacks without lots of nasty conditionals. For example, when a Foursquare user logs in, perhaps you want to refresh his Foursquare checkins data, but when a Facebook user logs in, you want to retrieve his latest list of friends.

Taking an Object-Oriented Approach

Most of these problems are caused by us squishing both OAuth users and Standard Users into a single User model even though their validations and requirements are quite different.

We are going to solve this by adding some Plain Old Ruby Objects.

Here's the general idea (we will go through the code shortly):

Standard User-specific logic and OAuth User-specific logic will be moved from the User model into RegularUser and OAuthUser models respectively. These will not be regular models. They will not be table-backed and won't inherit from ActiveRecord::Base. In many ways, they will be more similar to service objects than to models, and you may be more comfortable putting them in a services directory than in the models directory.

And here is the crucial bit: They will only be used in the User creation phase. They will act as intermediary layers between the core User model and the visitor using the website to create his User object.

The result is that the site visitor, creating or editing his account, always interacts via the RegularUser or OAuthUser class and its specific validations and logic. But once the user has been created, the app itself (e.g., the current_user helper) then interacts with the core User model directly, thereby bypassing any issues with RegularUser-specific or OAuthUser-specific validation.

Nice and clean.

Bring Out The Models!

Let's start modeling this out. The first model to create is an Account model. Each Account will represent a single OAuth account (Facebook, Twitter etc) and will belong to a User, so that a User can have multiple OAuth accounts.

rails g model Account user:references uid provider username oauth_token oauth_secret oauth_expires:datetime
app/models/account.rb
class Account < ActiveRecord::Base

  # Account records contain OAuth data from third parties: Facebook, Twitter, Foursquare, and so on.

  # Associations
  belongs_to :user

  # Attributes
  attr_accessible :user_id, :oauth_expires, :oauth_token, :provider, :uid, :username, :oauth_secret

end

Our user model will look pretty standard, except that it will contain no validations. It won't even have has_secure_password.

rails g model User first_name last_name email password_digest
app/models/user.rb
class User < ActiveRecord::Base

  # Associations
  has_many :accounts, :dependent => :destroy

  # Attributes
  attr_accessible :email, :first_name, :last_name, :password, :password_confirmation

  # Instance Methods
  def has_facebook?
    accounts.where(provider: 'facebook').any?
  end

  def has_twitter?
    accounts.where(provider: 'twitter').any?
  end

  def has_foursquare?
    accounts.where(provider: 'foursquare').any?
  end

end

So how do we deal with Standard Users - those users that sign up using a regular form rather than OAuth? We create a RegularUser model that inherits from User, and layers Standard User-specific logic on top.

app/models/regular_user.rb
class RegularUser < User

  has_secure_password

  validates :first_name, :last_name, :email, presence: true
  validates :email, email: true
  validates :email, uniqueness: { case_sensitive: false }

end

Now we update the Users Controller to use RegularUser. Remember that users signing up via OAuth will be redirected by the gems and your routes to the Sessions Controller. The only users using the User Controller are Standard Users. So we update the User Controller to use our new RegularUser object, thereby ensuring that all Standard Users will be required to pass standard validations, such as a valid password.

app/controllers/user_controller.rb
class UsersController < ApplicationController

  before_filter :authenticate_user!, only: [:edit, :update]

  def new
    @user = RegularUser.new
  end

  def create
    @user = RegularUser.new(params[:regular_user])
    if @user.save
      session[:user_id] = @user.id
      redirect_to root_path
    else
      render action: 'new'
    end
  end

  def show
    @user = current_user
  end

  # etc etc.

end

The very small catch here is that our user form, which is now interacting with a RegularUser object, tries submitting to /regular_users. Explicitly defining the submit URL in the form corrects this.

app/views/users/_form.html.haml
= simple_form_for @user, :url => (@user.new_record? ? users_path : user_path(@user)) do |f|
  = f.input :first_name
  = f.input :last_name
  = f.input :email
  = f.input :password
  = f.input :password_confirmation
  = f.button :submit

Perfect. Our Standard Users can sign up using a form which will force them to adhere to Standard User validations, without those validations bleeding over into our OAuth users and creating trouble there.

To reiterate, the entire point is that the RegularUser model acts as a shield between our visitor and the app. The app itself doesn't use RegularUser ever. It is able to use User directly and never worry about validations clashing or failing (for example, a password validation failing for an OAuth User that has none). For example, the app's standard current_user helper method will use User directly:

app/controllers/application_controller.rb
class ApplicationController

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

end

Now that our Standard Users are taken care of, let's move on to OAuth.

Plugging In OAuth

I want to create a model/object to deal with the creation and login of OAuth users specifically, which will have the ability to deal with every use-case. I want the Sessions Controller, which the OAuth gems redirect to, to simply pass the OAuth data and the current_user into this OAuth model, and let the OAuth model deal with every possible case on its own:

  • If a user is not logged in, create a new user account and log the user in.
  • If a user is already logged in, simply add this OAuth account to his existing User account, so that one user can have many different OAuth accounts.
  • If a user is not logged in, but this OAuth account has been used before, find the corresponding user and log him in.

This OAuth model will be named OAuthUser (in contrast to RegularUser).

app/models/o_auth_user.rb
class OAuthUser

  attr_reader :provider, :user

  def initialize creds, user = nil
    @auth         = creds
    @user         = user
    @provider     = @auth.provider
  end

end

But before we proceed beyond this small start, we have a problem to tackle.

If you've integrated multiple OAuth gems into a single app before, you'd have noticed that the hash of user data returned by the gems varies from gem to gem. Twitter returns no email address, but Facebook does. Twitter returns a user's entire name ("John Doe") whereas Facebook splits a user's first name from his last name. And so on.

You've probably dealt with this in the past by creating separate methods in your User model for each type of OAuth platform, a solution which rapidly turns the User model into an ugly mess. So how can we deal with the differences between the OAuth gems cleanly and simply?

Policy Objects to the Rescue

There are multiple ways of defining what a policy object is and how it differs from a service object, but the general idea is that a policy object only contains business rules to inform other objects. In other words, a policy object generally does no work on its own; it simply gives the other objects that do the work the information they need to get their job done.

Our OAuth problem here is an ideal use-case for policy objects.

We will make a separate policy object for each platform - one for Facebook, one for Twitter, and so on. These objects will be small and simple, and each will contain the exact same method names: first_name, last_name, email etc. We simply pass the policy object the complete hash we get from the OAuth gem, and let the policy object define the values. To get started, add a folder named policies under the app directory, restart your Rails app, and add the objects. Here are what my Facebook, Twitter and LinkedIn Policy objects look like.

app/policies/facebook_policy.rb
class FacebookPolicy

  def initialize auth
    @auth = auth
  end

  def first_name
    @auth.info.first_name
  end

  def last_name
    @auth.info.last_name
  end

  def email
    @auth.info.email
  end

  def username
    @auth.info.nickname
  end

  def image_url
    "http://graph.facebook.com/#{auth.info.nickname}/picture?type=large"
  end

  def uid
    @auth.uid
  end

  def oauth_token
    @auth.credentials.token
  end

  def oauth_expires
    Time.at(@auth.credentials.expires_at)
  end

  def oauth_secret
    nil
  end

  def create_callback account
    # Place any methods you want to trigger on Facebook OAuth creation here.
  end

  def refresh_callback account
    # Place any methods you want to trigger on subsequent Facebook OAuth logins here.
  end

end
app/policies/twitter_policy.rb
class TwitterPolicy

  def initialize auth
    @auth = auth
  end

  def first_name
    split_name.first
  end

  def last_name
    split_name.last
  end

  def email
    nil
  end

  def username
    @auth.info.nickname
  end

  def image_url
    "https://api.twitter.com/1/users/profile_image?screen_name=#{@auth.info.nickname}&size=original"
  end

  def uid
    @auth.uid
  end

  def oauth_token
    @auth.credentials.token
  end

  def oauth_expires
    nil
  end

  def oauth_secret
    @auth.credentials.secret
  end

  def create_callback account
    # Place any methods you want to trigger on Twitter OAuth creation here.
  end

  def refresh_callback account
    # Place any methods you want to trigger on Twitter OAuth creation here.
  end


  private

    def split_name
      name = @auth.info.name
      if name.include?(" ")
        last_name  = name.split(" ").last
        first_name = name.split(" ")[0...-1].join(" ")
      else
        first_name = name
        last_name  = nil
      end
      [first_name, last_name]
    end

end
app/policies/linkedin_policy.rb
class LinkedinPolicy

  def initialize auth
    @auth = auth
  end

  def first_name
    @auth.info.first_name
  end

  def last_name
    @auth.info.last_name
  end

  def email
    @auth.info.email
  end

  def username
    @auth.info.urls.public_profile
  end

  def image_url
    @auth.info.image
  end

  def uid
    @auth.uid
  end

  def oauth_token
    @auth.credentials.token
  end

  def oauth_expires
    nil
  end

  def oauth_secret
    @auth.credentials.secret
  end

  def create_callback account
    # Place any methods you want to trigger on LinkedIn OAuth creation here.
  end

  def refresh_callback account
    # Place any methods you want to trigger on LinkedIn OAuth creation here.
  end

end

The idea is that each of these objects follow the same pattern. They each have identical method names with the rules of how to obtain each piece of information. FacebookPolicy's email method returns the email from the data hash, whereas TwitterPolicy's email method returns nil because Twitter does not give that information.

So how do these policy objects interact with our app? Time to return to the OAuthUser model.

The OAuthUser Model

So coming back to the initialize method in our OAuthUser model, we can now grab the correct policy object, and assign it the the @policy instance variable.

app/models/o_auth_user.rb
class OAuthUser

  attr_reader :provider, :user

  def initialize creds, user = nil
    @auth         = creds
    @user         = user
    @provider     = @auth.provider
    @policy       = "#{@provider}_policy".classify.constantize.new(@auth)
  end

end

We can now call methods on @policy, such as @policy.first_name, confident that the policy object will return the correct information.

Let's flesh out the rest of the OAuthUser model. We'll create a method called login_or_create which will automatically deal with all the possible scenarios.

If a user is logged in to our app when he uses OAuth, the logged in user will be passed by the Sessions Controller into this OAuthUser model and will be assigned to the @user variable. So if we do have a user assigned to the @user variable, it's because there is a user currently logged in. In that scenario, we simply create a new Account record (remember, Account, which we dealt with at the very beginning of this article, is the resource that stores OAuth login data for users) and associate it with the logged in user.

If there is no user logged in, we first use the OAuth data given by the gem to check the Accounts table and see if this user has logged in to our app before. If he has, we log him in again. If not, we create a new User account.

app/models/o_auth_user.rb
class OAuthUser

  attr_reader :provider, :user

  def initialize creds, user = nil
    @auth         = creds
    @user         = user
    @provider     = @auth.provider
    @policy       = "#{@provider}_policy".classify.constantize.new(@auth)
  end

  def login_or_create
    logged_in? ? create_new_account : (login || create_new_account)
  end

  def logged_in?
    @user.present?
  end

end

Let's write the methods for logging in a user. We search the Account table to see if this user logging in with OAuth now has logged in before. If he has, we find the Account record and refresh the OAuth tokens data stored in the Account table.

class OAuthUser

  # truncated

  def login
    @account = Account.where(@auth.slice("provider", "uid")).first
    if @account.present?
      refresh_tokens
      @user = @account.user
      @policy.refresh_callback(@account)
    else
      false
    end
  end

  def refresh_tokens
    @account.update_attributes(
      :oauth_token   => @policy.oauth_token,
      :oauth_expires => @policy.oauth_expires,
      :oauth_secret  => @policy.oauth_secret
    )
  end

  # truncated

end

If we cannot find any user, we create one.

class OAuthUser

  # truncated

  def create_new_account
    create_new_user if @user.nil?

    unless account_already_exists?
      @account = @user.accounts.create!(
        :provider      => @provider,
        :uid           => @policy.uid,
        :oauth_token   => @policy.oauth_token,
        :oauth_expires => @policy.oauth_expires,
        :oauth_secret  => @policy.oauth_secret,
        :username      => @policy.username
      )

      @policy.create_callback(@account)
    end
  end

  def account_already_exists?
    @user.accounts.exists?(provider: @provider, uid: @policy.uid)
  end

  def create_new_user
    @user = User.create!(
      :first_name => @policy.first_name,
      :last_name  => @policy.last_name,
      :email      => @policy.email,
      :picture    => image
    )
  end

  def image
    image = open(URI.parse(@policy.image_url), :ssl_verify_mode => OpenSSL::SSL::VERIFY_NONE)
    def image.original_filename; base_uri.path.split('/').last; end
    image
  end

  # truncated

end

Note that we call @policy.create_callback(@account) after creating an account and @policy.refresh_callback(@account) after logging in. This allows you to call methods for specific platforms. If you wanted to do something only when a Facebook OAuth user logs in, such as get his latest list of Facebook friends, you could put that code in the FacebookPolicy object's refresh_callback method and it would be triggered for Facebook users only.

The complete OAuthUser model looks like this:

app/models/o_auth_user.rb
class OAuthUser

  attr_reader :provider, :user

  def initialize creds, user = nil
    @auth         = creds
    @user         = user
    @provider     = @auth.provider
    @policy       = "#{@provider}_policy".classify.constantize.new(@auth)
  end

  def login_or_create
    logged_in? ? create_new_account : (login || create_new_account)
  end

  def logged_in?
    @user.present?
  end


  private

    def login
      @account = Account.where(@auth.slice("provider", "uid")).first
      if @account.present?
        refresh_tokens
        @user = @account.user
        @policy.refresh_callback(@account)
      else
        false
      end
    end

    def account_already_exists?
      @user.accounts.exists?(provider: @provider, uid: @policy.uid)
    end

    def create_new_account
      create_new_user if @user.nil?

      unless account_already_exists?
        @account = @user.accounts.create!(
          :provider      => @provider,
          :uid           => @policy.uid,
          :oauth_token   => @policy.oauth_token,
          :oauth_expires => @policy.oauth_expires,
          :oauth_secret  => @policy.oauth_secret,
          :username      => @policy.username
        )

        @policy.create_callback(@account)
      end
    end

    def create_new_user
      @user = User.create!(
        :first_name => @policy.first_name,
        :last_name  => @policy.last_name,
        :email      => @policy.email,
        :picture    => image
      )
    end

    def image
      image = open(URI.parse(@policy.image_url), :ssl_verify_mode => OpenSSL::SSL::VERIFY_NONE)
      def image.original_filename; base_uri.path.split('/').last; end
      image
    end

    def refresh_tokens
      @account.update_attributes(
        :oauth_token   => @policy.oauth_token,
        :oauth_expires => @policy.oauth_expires,
        :oauth_secret  => @policy.oauth_secret
      )
    end

end

Last, let's update our Sessions Controller to use the new OAuthUser model.

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  def new
  end

  def create
    if request.env["omniauth.auth"].present?

      oauth = OAuthUser.new(request.env["omniauth.auth"], current_user)
      oauth.login_or_create
      session[:user_id] = oauth.user.id
      redirect_to root_path

    else
      user = RegularUser.find_by_email(params[:session][:email])
      if user && user.authenticate(params[:session][:password])
        session[:user_id]    = user.id
        redirect_to root_path

      else
        flash.now[:error] = "Invalid login credentials."
        render action: 'new'
      end
    end
  end

  def destroy
    session[:user_id] = nil
    redirect_to root_url
  end

end

Let's walk through that quickly: If request.env["omniauth.auth"] exists, our user has just used OAuth. In that case, we pass the OAuth gem data, as well as the current_user into the OAuthUser model. If a user is logged in, OAuthUser handles it by adding this new OAuth account to his accounts. If a user is not logged in user, current_user equates to nil, which OAuthUser handles by finding the user and logging him in, or creating the user account if none can be found.

If request.env["omniauth.auth"] does not exist, the user is using standard email login, in which case we use RegularUser instead to log him in.

And We're Done

Though this approach takes a long time to explain, it is really a simple and straightforward concept. We separate Standard User logic from OAuth User logic, ensuring the app gets tripped up by neither, and use Policy Objects to deal with the differences between the OAuth gems.

In doing so, we remove gobs of spaghetti code from our User model and replace it with a small set of confident and robust objects which will not break down or get in your way as you add more domain logic to your users.

The best bit is that once you code this once, you can simply drag and drop the files into any Rails app for an immediate yet robust OAuth implementation.

The code used in this tutorial is viewable on Github: https://github.com/davidlesches/clean-oauth-core