| @@ -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…" %> | ||
| </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 |