October 27, 2010

#19 Implementing Authentication - OmniAuth

With the troubleshooting done, we can now write the code for our user authentication interface.

The Routes

In the last post, we saw that we'll need to handle two routes for our authentication scheme: one for success and one for failure.
config/routes.rb
Arailsdemo::Application.routes.draw do
  match '/auth/:provider/callback', :to => 'sessions#create'
  match '/auth/failure', :to => 'sessions#failure'
  ...
end

OmniAuth Configuration

We configure OmniAuth to use the various authentication strategies. For the OAuth providers (Github, Facebook, and Twitter), we have to register our application with the respective website. Then we put those 'keys' and 'secrets' in the configuration.
config/initializers/omniauth.rb
require 'omniauth/openid'
require 'openid/store/memcache'

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :github, "githubkey", "githubsecret"
  provider :facebook, "facebookkey", "facebooksecret"
  provider :twitter, 'twitterkey', "twittersecret"
  provider :open_id, OpenID::Store::Memcache.new(Dalli::Client.new)
end
...

The Login Page

We use some 'AuthButtons' to make some fancy links to the OAuth and OpendID providers.
app/views/sessions/new.html.haml
- title "Login"

.loginLinks
  = link_to image_tag('github_logo.png', :size => "74x53", :alt => "Github"), '/auth/github', :title => 'Github'

  = link_to image_tag('google_64.png', :size => "64x64", :alt => "google"), '/auth/open_id?openid_url=google.com/accounts/o8/id', :title => 'Google'

  = link_to image_tag('facebook_64.png', :size => "64x64", :alt => "Facebook"), '/auth/facebook', :title => 'Facebook'
   
  = link_to image_tag('twitter_64.png', :size => "64x64", :alt => "Twitter"), '/auth/twitter', :title => 'Twitter'

  = link_to image_tag('yahoo_64.png', :size => "64x64", :alt => "Yahoo"), '/auth/open_id?openid_url=yahoo.com', :title => 'Yahoo'

  = link_to image_tag('openid_64.png', :size => "64x64", :alt => "OpenID"), '/auth/open_id', :title => 'OpenID'

%hr

%p You can login to this site ...
%p
  Learn about
  = link_to "OpenID", "http://openid.net/"
  or
  = link_to "OAuth", "http://oath.net"

The Sessions Controller

Our create action will handle the success response from the OpenID and OAuth providers. The information from these providers will be available in the controller's request.env['omniauth.auth'] variable. We store the name or email from this hash and redirect to the posts index page.

Failed authentications are handled through a separate method and simply redirected to the login page with a flash message.

Finally, we update our 'user_signed_in?' method and add a 'logged_out?' method.
app/controllers/sessions_controller.rb and app/controllers/application_controller.rb
class SessionsController < ApplicationController  
  def new
  end  
  
  def create
    reset_session  #  see http://guides.rubyonrails.org/security.html#session-fixation
    info = request.env["omniauth.auth"]
    session[:name] = info["user_info"]["name"] || info["user_info"]["email"] || info["user_info"]["nickname"] || "fellow Ruby on Rails enthusiast"


    redirect_to posts_url, :notice => "Welcome #{session[:name]}!"
  end
  
  def failure
    redirect_to login_url, :alert => 'Sorry, there was something wrong with your login attempt. Please try again.'
  end

  def destroy  
    reset_session  
    flash[:notice] = "Logged out." 
    redirect_to posts_url 
  end
end
________________________________________

# application_controller.rb

class ApplicationController < ActionController::Base
  helper_method :admin?, :user_signed_in?, :logged_out?
  ...
  def user_signed_in?
    !session[:name].blank?
  end

  def logged_out?
    !user_signed_in? && !admin?
  end
end

Updating the Nav Bar

We toggle the login and logout links based on the user's status and show the user's name if he/she is logged in. However, since we're caching the home page, we need to exclude the name from that page.
app/shared/nav_bar.html.haml
#navBar
  %ol
    %li= link_to 'Home', root_url
    %li= link_to 'Building This Site', posts_url
    - if admin?
      ...
      %li.logout= link_to "Logout", logout_path
    - elsif user_signed_in?
      - if controller.class != PagesController
        %li= link_to "Logout (#{session[:name]})", logout_url
      - else
        %li= link_to "Logout", logout_url
    - elsif logged_out?
      %li= link_to 'Login', login_url

Sessions Store

Instead of using the default CookieStore for storing our session information, we'll switch to keeping that information on the server side in our database. After configuring Rails to use the ActiveRecordStore, we run the rake take to generate the migration file.
config/initializers/session_store.rb and Terminal
# session_store.rb

# Arailsdemo::Application.config.session_store :cookie_store, :key => '_arailsdemo_session'

Arailsdemo::Application.config.session_store :active_record_store
________________________________________

# Terminal

> rake db:sessions:create
> rake db:migrate

Clearing Out the SessionStore Periodically

After reading the Rails security guide, we setup a means to remove old sessions. (Plus, we don't want the sessions table getting too big.) We use the code in the security guide to create a method that will delete the sessions that are more than one hour old. Then we create a rake task that will call that method. Lastly, we add the Heroku cron add-on. With this, every day our sessions table will be cleared, and effectively, a session will last up to 25 hours.
app/modesl/session.rb, lib/tasks/cron.rake and Terminal
# session.rb

class Session < ActiveRecord::Base
  def self.sweep(time = 1.hour)
    time = time.split.inject { |count, unit|
        count.to_i.send(unit)
      } if time.is_a?(String)
      
    delete_all "created_at < '#{time.ago.to_s(:db)}'"
  end
end
________________________________________

# cron.rake

desc "This task is called by the Heroku cron add-on"
task :cron => :environment do
  Session.sweep
end
________________________________________

# Terminal

> heroku addons:add cron:daily

    Comments

(Please login to submit comments.)



View All Posts