August 05, 2008
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
Aug 06, 2008
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.
Aug 06, 2008
Very good idea!
Aug 06, 2008
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.
Aug 06, 2008
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.
Aug 07, 2008
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. :)
Aug 07, 2008
Great article! Do you have plans to generalize this to other major web account sites such as Yahoo and Microsoft?
Aug 08, 2008
@Joel – Nope. No plans.
Aug 15, 2008
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.
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.