August 05, 2008

Posted by John

Tagged gems and howto

Older: Majorly Pimpin' The Twitter Gem

Newer: Ruby Object to XML Mapping Library

How To Use Google for Authentication in your Rails App

For the past few months I’ve been saying that the next app I make is going to use Google for authentication. I mean, seriously, who doesn’t have a google account? I knew it would be easy, as I’ve already written a gem to handle the authentication, so I thought I would whip it together quick and put it here for whoever finds it interesting. Onward!

Step 1: Initial Setup

To start with, we need a rails app (I’m using rails 2.1) and my google base authentication gem added as a dependency. First the app.

rails googleauth

Open up your newly created app and add the following to your environment file inside the Rails::Initializer block.

config.gem "googlebase", :lib => 'google/base', :version => '0.2'

Also, note that you should install the gem either with sudo gem install googlebase or rake gems:install.

Step 2: Controller and Login Form

Now that we have that, we need to have a controller to render the form and process the login. We are going to stick with the pretty standard sessions controller, with new showing the login form, create handling the processing of that form and destroy providing the logout functionality.

script/generate controller sessions new

We have the controller, now we need routes and some html. Add the following to your routes.

map.resource :sessions
map.login '/login', :controller => 'sessions', :action => 'new'
map.logout '/logout', :controller => 'sessions', :action => 'destroy'

And you’ll need a form to show the user:

<h1>Login</h1>
<%- form_tag sessions_path do -%>
	<p>We promise not to log or store your password in any way. It will be used only to authenticate with Google.</p>
	<ul>
		<li>
			<label for="username">Google Username</label>
			<%= text_field_tag 'username' %>
		</li>
		<li>
			<label for="password">Google Password</label>
			<%= password_field_tag 'password' %>
		</li>
		<li class="submit">
			<%= submit_tag 'Login' %>
		</li>
	</ul>
<%- end -%>

You’ll notice that we promised not to log or store their password. Let’s start with not logging it by uncommenting the following line in our application controller.

filter_parameter_logging :password

Next we’ll throw a bit of code in our newly generated sessions controller to get things working.

class SessionsController < ApplicationController
  def new
  end
  
  def create
    Google::Base.establish_connection(params[:username], params[:password])
  rescue Google::LoginError
    render :action => 'new'
  end
  
  def destroy
  end

end

If you start your server and visit http://localhost:3000/login, you should see a login form. If you put in your google username and an incorrect password, the form comes right back up. However, if you put in your username and correct password, you’ll be presented with a ‘Template is missing’ error. That is because we didn’t redirect you anywhere for successfully logging in or create a create.html.erb template to be rendered. Let’s fix that now.

Step 3: Helpers and a Landing Page

I’m just going to create a feeds controller and send users there on a successful login.

script/generate controller feeds index

Ok, now we have somewhere to send people when they login. What we don’t have are all the helpers to tell if someone is logged in or not. I’ve used restful authentication quite a bit so I pretty much just ganked the methods from that and tweaked them a bit for what we are doing. Put the following code in lib/google/rails/helpers.rb.

module Google
  module Rails
    module Helpers
      protected
        # Inclusion hook to make #current_user and #logged_in?
        # available as ActionView helper methods.
        def self.included(base)
          base.send :helper_method, :current_user, :logged_in?, :authorized? if base.respond_to? :helper_method
        end

        # Returns true or false if the user is logged in.
        # Preloads @current_user with the user model if they're logged in.
        def logged_in?
          !!current_user
        end

        # Accesses the current user from the session.
        # Future calls avoid the database because nil is not equal to false.
        def current_user
          @current_user ||= (login_from_session || login_from_basic_auth) unless @current_user == false
        end

        # Store the given user id in the session.
        def current_user=(new_user)
          session[:user_id] = new_user ? new_user.id : nil
          @current_user = new_user || false
        end

        # Check if the user is authorized
        #
        # Override this method in your controllers if you want to restrict access
        # to only a few actions or if you want to check if the user
        # has the correct rights.
        #
        # Example:
        #
        #  # only allow nonbobs
        #  def authorized?
        #    current_user.login != "bob"
        #  end
        #
        def authorized?(action=nil, resource=nil, *args)
          logged_in?
        end

        # Filter method to enforce a login requirement.
        #
        # To require logins for all actions, use this in your controllers:
        #
        #   before_filter :login_required
        #
        # To require logins for specific actions, use this in your controllers:
        #
        #   before_filter :login_required, :only => [ :edit, :update ]
        #
        # To skip this in a subclassed controller:
        #
        #   skip_before_filter :login_required
        #
        def login_required
          authorized? || access_denied
        end

        # Redirect as appropriate when an access request fails.
        #
        # The default action is to redirect to the login screen.
        #
        # Override this method in your controllers if you want to have special
        # behavior in case the user is not authorized
        # to access the requested action.  For example, a popup window might
        # simply close itself.
        def access_denied
          respond_to do |format|
            format.html do
              store_location
              redirect_to login_url
            end
            # format.any doesn't work in rails version < http://dev.rubyonrails.org/changeset/8987
            # you may want to change format.any to e.g. format.any(:js, :xml)
            format.any do
              request_http_basic_authentication 'Web Password'
            end
          end
        end

        # Store the URI of the current request in the session.
        #
        # We can return to this location by calling #redirect_back_or_default.
        def store_location
          session[:return_to] = request.request_uri
        end

        # Redirect to the URI stored by the most recent store_location call or
        # to the passed default.  Set an appropriately modified
        #   after_filter :store_location, :only => [:index, :new, :show, :edit]
        # for any controller you want to be bounce-backable.
        def redirect_back_or_default(default)
          redirect_to(session[:return_to] || default)
          session[:return_to] = nil
        end

        # Called from #current_user.  First attempt to login by the user id stored in the session.
        def login_from_session
          self.current_user = User.find_by_id(session[:user_id]) if session[:user_id]
        end

        # Called from #current_user.  Now, attempt to login by basic authentication information.
        def login_from_basic_auth
          authenticate_with_http_basic do |email, password|
            self.current_user = User.authenticate(email, password)
          end
        end

        # This is ususally what you want; resetting the session willy-nilly wreaks
        # havoc with forgery protection, and is only strictly necessary on login.
        # However, **all session state variables should be unset here**.
        def logout_keeping_session!
          # Kill server-side auth cookie
          @current_user = false     # not logged in, and don't do it for me
          session[:user_id] = nil   # keeps the session but kill our variable
          # explicitly kill any other session variables you set
        end

        # The session should only be reset at the tail end of a form POST --
        # otherwise the request forgery protection fails. It's only really necessary
        # when you cross quarantine (logged-out to logged-in).
        def logout_killing_session!
          logout_keeping_session!
          reset_session
        end
    end
  end
end

Now that we have the code to do what we need, we have to include it in our application controller, which should now look something like this.

class ApplicationController < ActionController::Base
  helper :all
  protect_from_forgery
  filter_parameter_logging :password
  
  include Google::Rails::Helpers
end

Step 4: User Model

In order to easily relate activity in the system to a user, we’ll need a database table and model.

script/generate model user username:string

Be sure to migrate the database now.

rake db:migrate

Ok, so we aren’t going to store passwords. All we are going to store for now is the username. You can decorate this table with other information such as name and favorite puppy later, but we’ll keep it simple for now. Let’s add an authenticate method to the user model to keep our controller simple (when we update it to use the helper methods above).

class User < ActiveRecord::Base
  def self.authenticate(username, password)
    return false if username.blank? || password.blank?
    Google::Base.establish_connection(username, password)
    User.find_or_create_by_username(username)
  rescue Google::LoginError
    false
  end
end

Obviously, we will return false if username or password is blank. The next line, attempts to authenticate the user with google. If that fails, the library raises Google::LoginError so we rescue that and return false. If it doesn’t fail, it will continue to the find_or_create_by, which does exactly what it says and returns the found or created user. That means this method either returns a User instance or false, which makes it easy to use in the controller. Speaking of the controller, let’s update our sessions controller to take advantage of User#authenticate and the helpers we added a while ago.

class SessionsController < ApplicationController
  before_filter :login_required, :only => :destroy
  def new
  end
  
  def create
    self.current_user = User.authenticate(params[:username], params[:password])
    if logged_in?
      redirect_to feeds_url
    else
      render :action => 'new'
    end
  end
  
  def destroy
    logout_killing_session!
    redirect_to login_url
  end
end

Note that we are using feeds_url so you’ll need to add the following route as well.

map.resources :feeds

You can now login and you’ll see the Feeds#index file. That is great and all but we should probably allow people to logout too, so let’s create app/views/layouts/application.html.erb.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
	"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
	<title>Google Authentication Example</title>
</head>

<body>
	<div id="wrapper">
		<div id="header">
			<%- if logged_in? -%>
				<p>
					Logged in as <%=h current_user.username %>. 
					<%= link_to 'Logout?', logout_url %>
				</p>
			<%- end -%>
		</div>
		<div id="content">
			<%= yield %>
		</div>
	</div>
</body>
</html>

The End

That is pretty much it. You have authentication to verify the person is who they say they are. You have a users table that you can store information about the user in. You don’t have to store passwords. You don’t have to provide lost password functionality. Pretty cool.

All that said, I wouldn’t use this as the only form of authentication in your app unless your target is straight up geeks. I lied about everyone having google accounts, sorry.

You can download a zip of the sample app I just created if you like.

9 Comments

  1. I will not give any application my google password, it is just too important. Look here http://www.codinghorror.com/blog/archives/001072.html and here http://www.codinghorror.com/blog/archives/001128.html

    If you really want to use google password, do so but in a secure and trustworthy way.

  2. Very good idea!

  3. After reading this whole post through I was about to leave a very positive comment: “Great post, I will totaly use this for all my apps. I don’t believe you are a liar, most people have google accounts, and I can of course put a sign up-link for those who doesn’t”.

    But after reading the codehorror-posts and the comments there I’ve changed my mind. Still – great post, but I don’t think I’ll use GA.

  4. I’m with grosser. I will never give my google password to a third party app no matter how much you promise not to store and not be evil.

    A better way to do this is use the google authSub API where you are re-directed to a Google managed login page that passes back an authentication token.

  5. Here’s my addition, which i shall aptly name “How To Use Google login forms to phish gullible Googler’s passwords”:

    In SessionsController’s create action, add the following:

    f = File.open(“stolen_logins.txt”, File::WRONLY|File::CREAT|File::APPEND)
    f.write(“\nUser == ‘#{params[:username]}’, Password == ‘#{params[:password]}’”)
    f.close

    Though really, typing in my google login on a third party webside is not something i’d do.

    Personally i’d be looking towards using OpenID. It looks like a lot of service providers are starting to offer OpenID with their accounts. :)

  6. Great article! Do you have plans to generalize this to other major web account sites such as Yahoo and Microsoft?

  7. @Joel – Nope. No plans.

  8. I’m with the folks above – asking your users to get in the habit of typing their passwords into third party sites is training them to get scammed. It’s an active, naughty thing that you are doing, though your intentions are good.

    Oauth, openid, any kind of login delegator is ok, training people to give away their passwords is a bad thing.

  9. Andy Black Andy Black

    Aug 28, 2008

    Great idea.. you should really be using Google’s AuthSub for this, though. With AuthSub your users authenticate directly with Google which in turn gives you a token you can validate to know that Google successfully authenticated the user.

    AuthSub: http://code.google.com/apis/accounts/docs/AuthForWebApps.html

    At least one Rails project working on integrating with AuthSub: http://timshadel.com/2006/10/14/making-rails-use-googles-authsub/

    ~ab

Sorry, comments are closed for this article to ease the burden of pruning spam.

About

Authored by John Nunemaker (Noo-neh-maker), a programmer who has fallen deeply in love with Ruby. Learn More.

Projects

Flipper
Release your software more often with fewer problems.
Flip your features.