@@ -0,0 +1,170 @@
# This module is responsible for adding OpenID functionality to Authlogic. Checkout the README for more info and please
# see the sub modules for detailed documentation.
module AuthlogicOpenid
# This module is responsible for adding in the OpenID functionality to your models. It hooks itself into the
# acts_as_authentic method provided by Authlogic.
module ActsAsAuthentic
# Adds in the neccesary modules for acts_as_authentic to include and also disabled password validation if
# OpenID is being used.
def self.included(klass)
klass.class_eval do
extend Config
add_acts_as_authentic_module(Methods, :prepend)
end
end

module Config
# Some OpenID providers support a lightweight profile exchange protocol, for those that do, you can require
# certain fields. This is convenient for new registrations, as it will basically fill out the fields in the
# form for them, so they don't have to re-type information already stored with their OpenID account.
#
# For more info and what fields you can use see: http://openid.net/specs/openid-simple-registration-extension-1_0.html
#
# * <tt>Default:</tt> []
# * <tt>Accepts:</tt> Array of symbols
def openid_required_fields(value = nil)
rw_config(:openid_required_fields, value, [])
end
alias_method :openid_required_fields=, :openid_required_fields

# Same as required_fields, but optional instead.
#
# * <tt>Default:</tt> []
# * <tt>Accepts:</tt> Array of symbols
def openid_optional_fields(value = nil)
rw_config(:openid_optional_fields, value, [])
end
alias_method :openid_optional_fields=, :openid_optional_fields
end

module Methods
# Set up some simple validations
def self.included(klass)
return if !klass.column_names.include?("openid_identifier")

klass.class_eval do
validates_uniqueness_of :openid_identifier, :scope => validations_scope, :if => :using_openid?
validate :validate_openid
validates_length_of_password_field_options validates_length_of_password_field_options.merge(:if => :validate_password_with_openid?)
validates_confirmation_of_password_field_options validates_confirmation_of_password_field_options.merge(:if => :validate_password_with_openid?)
validates_length_of_password_confirmation_field_options validates_length_of_password_confirmation_field_options.merge(:if => :validate_password_with_openid?)
end
end

# Set the openid_identifier field and also resets the persistence_token if this value changes.
def openid_identifier=(value)
write_attribute(:openid_identifier, value.blank? ? nil : OpenID.normalize_url(value))
reset_persistence_token if openid_identifier_changed?
rescue OpenID::DiscoveryFailure => e
@openid_error = e.message
end

# This is where all of the magic happens. This is where we hook in and add all of the OpenID sweetness.
#
# I had to take this approach because when authenticating with OpenID nonces and what not are stored in database
# tables. That being said, the whole save process for ActiveRecord is wrapped in a transaction. Trying to authenticate
# with OpenID in a transaction is not good because that transaction be get rolled back, thus reversing all of the OpenID
# inserts and making OpenID authentication fail every time. So We need to step outside of the transaction and do our OpenID
# madness.
#
# Another advantage of taking this approach is that we can set fields from their OpenID profile before we save the record,
# if their OpenID provider supports it.
def save(perform_validation = true, &block)
return false if perform_validation && block_given? && authenticate_with_openid? && !authenticate_with_openid
result = super
yield(result) if block_given?
result
end

private
def authenticate_with_openid
@openid_error = nil

if !openid_complete?
session_class.controller.session[:openid_attributes] = attributes_to_save
else
map_saved_attributes(session_class.controller.session[:openid_attributes])
session_class.controller.session[:openid_attributes] = nil
end

options = {
:required => self.class.openid_required_fields,
:optional => self.class.openid_optional_fields,
:return_to => session_class.controller.url_for(:for_model => "1"),
:method => :post }

session_class.controller.send(:authenticate_with_open_id, openid_identifier, options) do |result, openid_identifier, registration|
if result.unsuccessful?
@openid_error = result.message
else
self.openid_identifier = openid_identifier
map_openid_registration(registration)
end

return true
end

return false
end

# Override this method to map the OpenID registration fields with fields in your model. See the required_fields and
# optional_fields configuration options to enable this feature.
#
# Basically you will get a hash of values passed as a single argument. Then just map them as you see fit. Check out
# the source of this method for an example.
def map_openid_registration(registration) # :doc:
self.name ||= registration[:fullname] if respond_to?(:name) && !registration[:fullname].blank?
self.first_name ||= registration[:fullname].split(" ").first if respond_to?(:first_name) && !registration[:fullname].blank?
self.last_name ||= registration[:fullname].split(" ").last if respond_to?(:last_name) && !registration[:last_name].blank?
end

# This method works in conjunction with map_saved_attributes.
#
# Let's say a user fills out a registration form, provides an OpenID and submits the form. They are then redirected to their
# OpenID provider. All is good and they are redirected back. All of those fields they spent time filling out are forgetten
# and they have to retype them all. To avoid this, AuthlogicOpenid saves all of these attributes in the session and then
# attempts to restore them. See the source for what attributes it saves. If you need to block more attributes, or save
# more just override this method and do whatever you want.
def attributes_to_save # :doc:
attrs_to_save = attributes.clone.delete_if do |k, v|
[:id, :password, crypted_password_field, password_salt_field, :persistence_token, :perishable_token, :single_access_token, :login_count,
:failed_login_count, :last_request_at, :current_login_at, :last_login_at, :current_login_ip, :last_login_ip, :created_at,
:updated_at, :lock_version].include?(k.to_sym)
end
attrs_to_save.merge!(:password => password, :password_confirmation => password_confirmation)
end

# This method works in conjunction with attributes_to_save. See that method for a description of the why these methods exist.
#
# If the default behavior of this method is not sufficient for you because you have attr_protected or attr_accessible then
# override this method and set them individually. Maybe something like this would be good:
#
# attrs.each do |key, value|
# send("#{key}=", value)
# end
def map_saved_attributes(attrs) # :doc:
self.attributes = attrs
end

def validate_openid
errors.add(:openid_identifier, "had the following error: #{@openid_error}") if @openid_error
end

def using_openid?
respond_to?(:openid_identifier) && !openid_identifier.blank?
end

def openid_complete?
session_class.controller.using_open_id? && session_class.controller.params[:for_model]
end

def authenticate_with_openid?
session_class.activated? && ((using_openid? && openid_identifier_changed?) || openid_complete?)
end

def validate_password_with_openid?
!using_openid? && require_password?
end
end
end
end
@@ -0,0 +1,137 @@
module AuthlogicOpenid
# This module is responsible for adding all of the OpenID goodness to the Authlogic::Session::Base class.
module Session
# Add a simple openid_identifier attribute and some validations for the field.
def self.included(klass)
klass.class_eval do
extend Config
include Methods
end
end

module Config
# What method should we call to find a record by the openid_identifier?
# This is useful if you want to store multiple openid_identifiers for a single record.
# You could do something like:
#
# class User < ActiveRecord::Base
# def self.find_by_openid_identifier(identifier)
# user.first(:conditions => {:openid_identifiers => {:identifier => identifier}})
# end
# end
#
# Obviously the above depends on what you are calling your assocition, etc. But you get the point.
#
# * <tt>Default:</tt> :find_by_openid_identifier
# * <tt>Accepts:</tt> Symbol
def find_by_openid_identifier_method(value = nil)
rw_config(:find_by_openid_identifier_method, value, :find_by_openid_identifier)
end
alias_method :find_by_openid_identifier_method=, :find_by_openid_identifier_method

# Add this in your Session object to Auto Register a new user using openid via sreg
def auto_register(value=true)
auto_register_value(value)
end

def auto_register_value(value=nil)
rw_config(:auto_register,value,false)
end

alias_method :auto_register=,:auto_register
end

module Methods
def self.included(klass)
klass.class_eval do
attr_reader :openid_identifier
validate :validate_openid_error
validate :validate_by_openid, :if => :authenticating_with_openid?
end
end

# Hooks into credentials so that you can pass an :openid_identifier key.
def credentials=(value)
super
values = value.is_a?(Array) ? value : [value]
hash = values.first.is_a?(Hash) ? values.first.with_indifferent_access : nil
self.openid_identifier = hash[:openid_identifier] if !hash.nil? && hash.key?(:openid_identifier)
end

def openid_identifier=(value)
@openid_identifier = value.blank? ? nil : OpenID.normalize_url(value)
@openid_error = nil
rescue OpenID::DiscoveryFailure => e
@openid_identifier = nil
@openid_error = e.message
end

# Cleaers out the block if we are authenticating with OpenID, so that we can redirect without a DoubleRender
# error.
def save(&block)
block = nil if !openid_identifier.blank? && controller.request.env[Rack::OpenID::RESPONSE].blank?
super(&block)
end

private
def authenticating_with_openid?
attempted_record.nil? && errors.empty? && (!openid_identifier.blank? || (controller.using_open_id? && controller.params[:for_session]))
end

def find_by_openid_identifier_method
self.class.find_by_openid_identifier_method
end

def find_by_openid_identifier_method
self.class.find_by_openid_identifier_method
end

def auto_register?
self.class.auto_register_value
end

def validate_by_openid
self.remember_me = controller.params[:remember_me] == "true" if controller.params.key?(:remember_me)

options = {
:required => klass.openid_required_fields,
:optional => klass.openid_optional_fields,
:return_to => controller.url_for(:for_session => "1", :remember_me => remember_me?),
:method => :post}

controller.send(:authenticate_with_open_id, openid_identifier, options) do |result, openid_identifier, registration|
if result.unsuccessful?
errors.add_to_base(result.message)
return
end

self.attempted_record = klass.send(find_by_openid_identifier_method, openid_identifier)

if !attempted_record
if auto_register?
auto_reg_record = create_open_id_auto_register_record(openid_identifier, registration)
if !auto_reg_record.save_without_session_maintenance
auto_reg_record.errors.each {|attr, msg| errors.add(attr, msg) }
else
self.attempted_record = auto_reg_record
end
else
errors.add(:openid_identifier, "did not match any users in our database, have you set up your account to use OpenID?")
end
end
end
end

def create_open_id_auto_register_record(openid_identifier, registration)
returning klass.new do |auto_reg_record|
auto_reg_record.openid_identifier = openid_identifier
auto_reg_record.send(:map_openid_registration, registration)
end
end

def validate_openid_error
errors.add(:openid_identifier, @openid_error) if @openid_error
end
end
end
end
@@ -0,0 +1,51 @@
module AuthlogicOpenid
# A class for describing the current version of a library. The version
# consists of three parts: the +major+ number, the +minor+ number, and the
# +tiny+ (or +patch+) number.
class Version
include Comparable

# A convenience method for instantiating a new Version instance with the
# given +major+, +minor+, and +tiny+ components.
def self.[](major, minor, tiny)
new(major, minor, tiny)
end

attr_reader :major, :minor, :tiny

# Create a new Version object with the given components.
def initialize(major, minor, tiny)
@major, @minor, @tiny = major, minor, tiny
end

# Compare this version to the given +version+ object.
def <=>(version)
to_i <=> version.to_i
end

# Converts this version object to a string, where each of the three
# version components are joined by the '.' character. E.g., 2.0.0.
def to_s
@to_s ||= [@major, @minor, @tiny].join(".")
end

# Converts this version to a canonical integer that may be compared
# against other version objects.
def to_i
@to_i ||= @major * 1_000_000 + @minor * 1_000 + @tiny
end

def to_a
[@major, @minor, @tiny]
end

MAJOR = 1
MINOR = 0
TINY = 4

# The current version as a Version instance
CURRENT = new(MAJOR, MINOR, TINY)
# The current version as a String
STRING = CURRENT.to_s
end
end
@@ -0,0 +1 @@
require "authlogic_openid"
@@ -0,0 +1,105 @@
require File.dirname(__FILE__) + '/test_helper.rb'

class ActsAsAuthenticTest < ActiveSupport::TestCase
def test_included
assert User.send(:acts_as_authentic_modules).include?(AuthlogicOpenid::ActsAsAuthentic::Methods)
assert_equal :validate_password_with_openid?, User.validates_length_of_password_field_options[:if]
assert_equal :validate_password_with_openid?, User.validates_confirmation_of_password_field_options[:if]
assert_equal :validate_password_with_openid?, User.validates_length_of_password_confirmation_field_options[:if]
end

def test_password_not_required_on_create
user = User.new
user.login = "sweet"
user.email = "a@a.com"
user.openid_identifier = "https://me.yahoo.com/a/9W0FJjRj0o981TMSs0vqVxPdmMUVOQ--"
assert !user.save {} # because we are redirecting, the user was NOT saved
assert_redirecting_to_yahoo "for_model"
end

def test_password_required_on_create
user = User.new
user.login = "sweet"
user.email = "a@a.com"
assert !user.save
assert user.errors.on(:password)
assert user.errors.on(:password_confirmation)
end

def test_password_not_required_on_update
ben = users(:ben)
assert_nil ben.crypted_password
assert ben.save
end

def test_password_required_on_update
ben = users(:ben)
ben.openid_identifier = nil
assert_nil ben.crypted_password
assert !ben.save
assert ben.errors.on(:password)
assert ben.errors.on(:password_confirmation)
end

def test_validates_uniqueness_of_openid_identifier
u = User.new(:openid_identifier => "bens_identifier")
assert !u.valid?
assert u.errors.on(:openid_identifier)
end

def test_setting_openid_identifier_changed_persistence_token
ben = users(:ben)
old_persistence_token = ben.persistence_token
ben.openid_identifier = "http://new"
assert_not_equal old_persistence_token, ben.persistence_token
end

def test_invalid_openid_identifier
u = User.new(:openid_identifier => "%")
assert !u.valid?
assert u.errors.on(:openid_identifier)
end

def test_blank_openid_identifer_gets_set_to_nil
u = User.new(:openid_identifier => "")
assert_nil u.openid_identifier
end

def test_updating_with_openid
ben = users(:ben)
ben.openid_identifier = "https://me.yahoo.com/a/9W0FJjRj0o981TMSs0vqVxPdmMUVOQ--"
assert !ben.save {} # because we are redirecting
assert_redirecting_to_yahoo "for_model"
end

def test_updating_without_openid
ben = users(:ben)
ben.openid_identifier = nil
ben.password = "test"
ben.password_confirmation = "test"
assert ben.save
assert_not_redirecting
end

def test_updating_without_validation
ben = users(:ben)
ben.openid_identifier = "https://me.yahoo.com/a/9W0FJjRj0o981TMSs0vqVxPdmMUVOQ--"
assert ben.save(false)
assert_not_redirecting
end

def test_updating_without_a_block
ben = users(:ben)
ben.openid_identifier = "https://me.yahoo.com/a/9W0FJjRj0o981TMSs0vqVxPdmMUVOQ--"
assert ben.save
ben.reload
assert_equal "https://me.yahoo.com/a/9W0FJjRj0o981TMSs0vqVxPdmMUVOQ--", ben.openid_identifier
end

def test_updating_while_not_activated
UserSession.controller = nil
ben = users(:ben)
ben.openid_identifier = "https://me.yahoo.com/a/9W0FJjRj0o981TMSs0vqVxPdmMUVOQ--"
assert ben.save {}
end
end
@@ -0,0 +1,9 @@
ben:
login: bjohnson
persistence_token: 6cde0674657a8a313ce952df979de2830309aa4c11ca65805dd00bfdc65dbcc2f5e36718660a1d2e68c1a08c276d996763985d2f06fd3d076eb7bc4d97b1e317
single_access_token: <%= Authlogic::Random.friendly_token %>
perishable_token: <%= Authlogic::Random.friendly_token %>
openid_identifier: bens_identifier
email: bjohnson@binarylogic.com
first_name: Ben
last_name: Johnson
@@ -0,0 +1,3 @@
class User < ActiveRecord::Base
acts_as_authentic
end
@@ -0,0 +1,2 @@
class UserSession < Authlogic::Session::Base
end
@@ -0,0 +1,32 @@
require File.dirname(__FILE__) + '/test_helper.rb'

class SessionTest < ActiveSupport::TestCase
def test_openid_identifier
session = UserSession.new
assert session.respond_to?(:openid_identifier)
session.openid_identifier = "http://test"
assert_equal "http://test/", session.openid_identifier
end

def test_validate_openid_error
session = UserSession.new
session.openid_identifier = "yes"
session.openid_identifier = "%"
assert_nil session.openid_identifier
assert !session.save
assert session.errors.on(:openid_identifier)
end

def test_validate_by_nil_openid_identifier
session = UserSession.new
assert !session.save
assert_not_redirecting
end

def test_validate_by_correct_openid_identifier
session = UserSession.new
session.openid_identifier = "https://me.yahoo.com/a/9W0FJjRj0o981TMSs0vqVxPdmMUVOQ--"
assert !session.save
assert_redirecting_to_yahoo "for_session"
end
end
@@ -0,0 +1,117 @@
require "test/unit"
require "rubygems"
require "ruby-debug"
require "active_record"
require "action_controller"
require "action_controller/test_process"

ActiveRecord::Schema.verbose = false
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
ActiveRecord::Base.configurations = true
ActiveRecord::Schema.define(:version => 1) do
create_table :open_id_authentication_associations, :force => true do |t|
t.integer :issued, :lifetime
t.string :handle, :assoc_type
t.binary :server_url, :secret
end

create_table :open_id_authentication_nonces, :force => true do |t|
t.integer :timestamp, :null => false
t.string :server_url, :null => true
t.string :salt, :null => false
end

create_table :users do |t|
t.datetime :created_at
t.datetime :updated_at
t.integer :lock_version, :default => 0
t.string :login
t.string :crypted_password
t.string :password_salt
t.string :persistence_token
t.string :single_access_token
t.string :perishable_token
t.string :openid_identifier
t.string :email
t.string :first_name
t.string :last_name
t.integer :login_count, :default => 0, :null => false
t.integer :failed_login_count, :default => 0, :null => false
t.datetime :last_request_at
t.datetime :current_login_at
t.datetime :last_login_at
t.string :current_login_ip
t.string :last_login_ip
end
end

require "active_record/fixtures"
require "openid"

module Rails
module VERSION
STRING = "2.3.5"
end
end

require File.dirname(__FILE__) + "/../../authlogic/lib/authlogic"
require File.dirname(__FILE__) + "/../../authlogic/lib/authlogic/test_case"
require File.dirname(__FILE__) + '/../../open_id_authentication/lib/open_id_authentication'

# this is partly from open_id_authentication/init.rb
ActionController::Base.send :include, OpenIdAuthentication

require File.dirname(__FILE__) + '/../lib/authlogic_openid' unless defined?(AuthlogicOpenid)
require File.dirname(__FILE__) + '/libs/user'
require File.dirname(__FILE__) + '/libs/user_session'

ActionController::Routing::Routes.draw do |map|
map.connect ':controller/:action/:id', :controller => 'session'
end

class SessionController < ActionController::Base
def default_template(action_name = self.action_name)
nil
end
end

class ActiveSupport::TestCase
include ActiveRecord::TestFixtures
self.fixture_path = File.dirname(__FILE__) + "/fixtures"
self.use_transactional_fixtures = false
self.use_instantiated_fixtures = false
self.pre_loaded_fixtures = false
fixtures :all
setup :activate_authlogic

private

def controller
@controller ||= create_controller
end

def create_controller
@request = ActionController::TestRequest.new
@request.path_parameters = {:action => "index", :controller => "session"}
@response = ActionController::TestResponse.new

c = SessionController.new
c.params = {}
c.request = @request
c.response = @response
c.send(:reset_session)
c.send(:initialize_current_url)

Authlogic::ControllerAdapters::RailsAdapter.new(c)
end

def assert_redirecting_to_yahoo(for_param)
[ /^OpenID identifier="https:\/\/me.yahoo.com\/a\/9W0FJjRj0o981TMSs0vqVxPdmMUVOQ--"/,
/return_to=\"http:\/\/test.host\/\?#{for_param}=1"/,
/method="post"/ ].each {|p| assert_match p, @response.headers["WWW-Authenticate"]}
end

def assert_not_redirecting
assert ! @response.headers["WWW-Authenticate"]
end
end
@@ -0,0 +1,37 @@
* Dump heavy lifting off to rack-openid gem. OpenIdAuthentication is just a simple controller concern.

* Fake HTTP method from OpenID server since they only support a GET. Eliminates the need to set an extra route to match the server's reply. [Josh Peek]

* OpenID 2.0 recommends that forms should use the field name "openid_identifier" rather than "openid_url" [Josh Peek]

* Return open_id_response.display_identifier to the application instead of .endpoints.claimed_id. [nbibler]

* Add Timeout protection [Rick]

* An invalid identity url passed through authenticate_with_open_id will no longer raise an InvalidOpenId exception. Instead it will return Result[:missing] to the completion block.

* Allow a return_to option to be used instead of the requested url [Josh Peek]

* Updated plugin to use Ruby OpenID 2.x.x [Josh Peek]

* Tied plugin to ruby-openid 1.1.4 gem until we can make it compatible with 2.x [DHH]

* Use URI instead of regexps to normalize the URL and gain free, better matching #8136 [dkubb]

* Allow -'s in #normalize_url [Rick]

* remove instance of mattr_accessor, it was breaking tests since they don't load ActiveSupport. Fix Timeout test [Rick]

* Throw a InvalidOpenId exception instead of just a RuntimeError when the URL can't be normalized [DHH]

* Just use the path for the return URL, so extra query parameters don't interfere [DHH]

* Added a new default database-backed store after experiencing trouble with the filestore on NFS. The file store is still available as an option [DHH]

* Added normalize_url and applied it to all operations going through the plugin [DHH]

* Removed open_id? as the idea of using the same input box for both OpenID and username has died -- use using_open_id? instead (which checks for the presence of params[:openid_url] by default) [DHH]

* Added OpenIdAuthentication::Result to make it easier to deal with default situations where you don't care to do something particular for each error state [DHH]

* Stop relying on root_url being defined, we can just grab the current url instead [DHH]
@@ -0,0 +1,223 @@
OpenIdAuthentication
====================

Provides a thin wrapper around the excellent ruby-openid gem from JanRan. Be sure to install that first:

gem install ruby-openid

To understand what OpenID is about and how it works, it helps to read the documentation for lib/openid/consumer.rb
from that gem.

The specification used is http://openid.net/specs/openid-authentication-2_0.html.


Prerequisites
=============

OpenID authentication uses the session, so be sure that you haven't turned that off.

Alternatively, you can use the file-based store, which just relies on on tmp/openids being present in RAILS_ROOT. But be aware that this store only works if you have a single application server. And it's not safe to use across NFS. It's recommended that you use the database store if at all possible. To use the file-based store, you'll also have to add this line to your config/environment.rb:

OpenIdAuthentication.store = :file

This particular plugin also relies on the fact that the authentication action allows for both POST and GET operations.
If you're using RESTful authentication, you'll need to explicitly allow for this in your routes.rb.

The plugin also expects to find a root_url method that points to the home page of your site. You can accomplish this by using a root route in config/routes.rb:

map.root :controller => 'articles'

This plugin relies on Rails Edge revision 6317 or newer.


Example
=======

This example is just to meant to demonstrate how you could use OpenID authentication. You might well want to add
salted hash logins instead of plain text passwords and other requirements on top of this. Treat it as a starting point,
not a destination.

Note that the User model referenced in the simple example below has an 'identity_url' attribute. You will want to add the same or similar field to whatever
model you are using for authentication.

Also of note is the following code block used in the example below:

authenticate_with_open_id do |result, identity_url|
...
end

In the above code block, 'identity_url' will need to match user.identity_url exactly. 'identity_url' will be a string in the form of 'http://example.com' -
If you are storing just 'example.com' with your user, the lookup will fail.

There is a handy method in this plugin called 'normalize_url' that will help with validating OpenID URLs.

OpenIdAuthentication.normalize_url(user.identity_url)

The above will return a standardized version of the OpenID URL - the above called with 'example.com' will return 'http://example.com/'
It will also raise an InvalidOpenId exception if the URL is determined to not be valid.
Use the above code in your User model and validate OpenID URLs before saving them.

config/routes.rb

map.root :controller => 'articles'
map.resource :session


app/views/sessions/new.erb

<% form_tag(session_url) do %>
<p>
<label for="name">Username:</label>
<%= text_field_tag "name" %>
</p>

<p>
<label for="password">Password:</label>
<%= password_field_tag %>
</p>

<p>
...or use:
</p>

<p>
<label for="openid_identifier">OpenID:</label>
<%= text_field_tag "openid_identifier" %>
</p>

<p>
<%= submit_tag 'Sign in', :disable_with => "Signing in&hellip;" %>
</p>
<% end %>

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def create
if using_open_id?
open_id_authentication
else
password_authentication(params[:name], params[:password])
end
end


protected
def password_authentication(name, password)
if @current_user = @account.users.authenticate(params[:name], params[:password])
successful_login
else
failed_login "Sorry, that username/password doesn't work"
end
end

def open_id_authentication
authenticate_with_open_id do |result, identity_url|
if result.successful?
if @current_user = @account.users.find_by_identity_url(identity_url)
successful_login
else
failed_login "Sorry, no user by that identity URL exists (#{identity_url})"
end
else
failed_login result.message
end
end
end


private
def successful_login
session[:user_id] = @current_user.id
redirect_to(root_url)
end

def failed_login(message)
flash[:error] = message
redirect_to(new_session_url)
end
end



If you're fine with the result messages above and don't need individual logic on a per-failure basis,
you can collapse the case into a mere boolean:

def open_id_authentication
authenticate_with_open_id do |result, identity_url|
if result.successful? && @current_user = @account.users.find_by_identity_url(identity_url)
successful_login
else
failed_login(result.message || "Sorry, no user by that identity URL exists (#{identity_url})")
end
end
end


Simple Registration OpenID Extension
====================================

Some OpenID Providers support this lightweight profile exchange protocol. See more: http://www.openidenabled.com/openid/simple-registration-extension

You can support it in your app by changing #open_id_authentication

def open_id_authentication(identity_url)
# Pass optional :required and :optional keys to specify what sreg fields you want.
# Be sure to yield registration, a third argument in the #authenticate_with_open_id block.
authenticate_with_open_id(identity_url,
:required => [ :nickname, :email ],
:optional => :fullname) do |result, identity_url, registration|
case result.status
when :missing
failed_login "Sorry, the OpenID server couldn't be found"
when :invalid
failed_login "Sorry, but this does not appear to be a valid OpenID"
when :canceled
failed_login "OpenID verification was canceled"
when :failed
failed_login "Sorry, the OpenID verification failed"
when :successful
if @current_user = @account.users.find_by_identity_url(identity_url)
assign_registration_attributes!(registration)

if current_user.save
successful_login
else
failed_login "Your OpenID profile registration failed: " +
@current_user.errors.full_messages.to_sentence
end
else
failed_login "Sorry, no user by that identity URL exists"
end
end
end
end

# registration is a hash containing the valid sreg keys given above
# use this to map them to fields of your user model
def assign_registration_attributes!(registration)
model_to_registration_mapping.each do |model_attribute, registration_attribute|
unless registration[registration_attribute].blank?
@current_user.send("#{model_attribute}=", registration[registration_attribute])
end
end
end

def model_to_registration_mapping
{ :login => 'nickname', :email => 'email', :display_name => 'fullname' }
end

Attribute Exchange OpenID Extension
===================================

Some OpenID providers also support the OpenID AX (attribute exchange) protocol for exchanging identity information between endpoints. See more: http://openid.net/specs/openid-attribute-exchange-1_0.html

Accessing AX data is very similar to the Simple Registration process, described above -- just add the URI identifier for the AX field to your :optional or :required parameters. For example:

authenticate_with_open_id(identity_url,
:required => [ :email, 'http://schema.openid.net/birthDate' ]) do |result, identity_url, registration|

This would provide the sreg data for :email, and the AX data for 'http://schema.openid.net/birthDate'



Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license
@@ -0,0 +1,12 @@
if Rails.version < '3'
config.gem 'rack-openid', :lib => 'rack/openid', :version => '>=0.2.1'
end

require 'open_id_authentication'

config.middleware.use OpenIdAuthentication

config.after_initialize do
OpenID::Util.logger = Rails.logger
ActionController::Base.send :include, OpenIdAuthentication
end
@@ -0,0 +1,128 @@
require 'uri'
require 'openid'
require 'rack/openid'

module OpenIdAuthentication
def self.new(app)
store = OpenIdAuthentication.store
if store.nil?
Rails.logger.warn "OpenIdAuthentication.store is nil. Using in-memory store."
end

::Rack::OpenID.new(app, OpenIdAuthentication.store)
end

def self.store
@@store
end

def self.store=(*store_option)
store, *parameters = *([ store_option ].flatten)

@@store = case store
when :memory
require 'openid/store/memory'
OpenID::Store::Memory.new
when :file
require 'openid/store/filesystem'
OpenID::Store::Filesystem.new(Rails.root.join('tmp/openids'))
when :memcache
require 'memcache'
require 'openid/store/memcache'
OpenID::Store::Memcache.new(MemCache.new(parameters))
else
store
end
end

self.store = nil

class Result
ERROR_MESSAGES = {
:missing => "Sorry, the OpenID server couldn't be found",
:invalid => "Sorry, but this does not appear to be a valid OpenID",
:canceled => "OpenID verification was canceled",
:failed => "OpenID verification failed",
:setup_needed => "OpenID verification needs setup"
}

def self.[](code)
new(code)
end

def initialize(code)
@code = code
end

def status
@code
end

ERROR_MESSAGES.keys.each { |state| define_method("#{state}?") { @code == state } }

def successful?
@code == :successful
end

def unsuccessful?
ERROR_MESSAGES.keys.include?(@code)
end

def message
ERROR_MESSAGES[@code]
end
end

protected
# The parameter name of "openid_identifier" is used rather than
# the Rails convention "open_id_identifier" because that's what
# the specification dictates in order to get browser auto-complete
# working across sites
def using_open_id?(identifier = nil) #:doc:
identifier ||= open_id_identifier
!identifier.blank? || request.env[Rack::OpenID::RESPONSE]
end

def authenticate_with_open_id(identifier = nil, options = {}, &block) #:doc:
identifier ||= open_id_identifier

if request.env[Rack::OpenID::RESPONSE]
complete_open_id_authentication(&block)
else
begin_open_id_authentication(identifier, options, &block)
end
end

private
def open_id_identifier
params[:openid_identifier] || params[:openid_url]
end

def begin_open_id_authentication(identifier, options = {})
options[:identifier] = identifier
value = Rack::OpenID.build_header(options)
response.headers[Rack::OpenID::AUTHENTICATE_HEADER] = value
head :unauthorized
end

def complete_open_id_authentication
response = request.env[Rack::OpenID::RESPONSE]
identifier = response.display_identifier

case response.status
when OpenID::Consumer::SUCCESS
yield Result[:successful], identifier,
OpenID::SReg::Response.from_success_response(response)
when :missing
yield Result[:missing], identifier, nil
when :invalid
yield Result[:invalid], identifier, nil
when OpenID::Consumer::CANCEL
yield Result[:canceled], identifier, nil
when OpenID::Consumer::FAILURE
yield Result[:failed], identifier, nil
when OpenID::Consumer::SETUP_NEEDED
yield Result[:setup_needed], response.setup_url, nil
end
end
end