From 83f05ca01b03e5000f0772e119c7a5a6e5d305c0 Mon Sep 17 00:00:00 2001 From: binarylogic Date: Sun, 29 Mar 2009 23:28:22 -0400 Subject: [PATCH] Release v1.0 --- CHANGELOG.rdoc | 3 + README.rdoc | 2 - Rakefile | 4 +- init.rb | 3 +- lib/authlogic_openid/acts_as_authentic.rb | 32 ++- lib/authlogic_openid/session.rb | 22 +- lib/authlogic_openid/version.rb | 11 +- test/acts_as_authentic_test.rb | 90 +++++++ test/fixtures/users.yml | 9 + test/libs/open_id_authentication/CHANGELOG | 35 +++ test/libs/open_id_authentication/README | 231 +++++++++++++++++ test/libs/open_id_authentication/Rakefile | 22 ++ ...open_id_authentication_tables_generator.rb | 11 + .../templates/migration.rb | 20 ++ .../templates/migration.rb | 26 ++ ...open_id_authentication_tables_generator.rb | 11 + test/libs/open_id_authentication/init.rb | 18 ++ .../lib/open_id_authentication.rb | 244 ++++++++++++++++++ .../lib/open_id_authentication/association.rb | 9 + .../lib/open_id_authentication/db_store.rb | 55 ++++ .../open_id_authentication/mem_cache_store.rb | 73 ++++++ .../lib/open_id_authentication/nonce.rb | 5 + .../lib/open_id_authentication/request.rb | 23 ++ .../open_id_authentication/timeout_fixes.rb | 20 ++ .../tasks/open_id_authentication_tasks.rake | 30 +++ .../test/mem_cache_store_test.rb | 151 +++++++++++ .../test/normalize_test.rb | 32 +++ .../test/open_id_authentication_test.rb | 46 ++++ .../test/status_test.rb | 14 + .../test/test_helper.rb | 17 ++ test/libs/rails_trickery.rb | 41 +++ test/libs/user.rb | 3 + test/libs/user_session.rb | 2 + test/session_test.rb | 32 +++ test/test_helper.rb | 78 ++++++ 35 files changed, 1402 insertions(+), 23 deletions(-) create mode 100644 test/acts_as_authentic_test.rb create mode 100644 test/fixtures/users.yml create mode 100644 test/libs/open_id_authentication/CHANGELOG create mode 100644 test/libs/open_id_authentication/README create mode 100644 test/libs/open_id_authentication/Rakefile create mode 100644 test/libs/open_id_authentication/generators/open_id_authentication_tables/open_id_authentication_tables_generator.rb create mode 100644 test/libs/open_id_authentication/generators/open_id_authentication_tables/templates/migration.rb create mode 100644 test/libs/open_id_authentication/generators/upgrade_open_id_authentication_tables/templates/migration.rb create mode 100644 test/libs/open_id_authentication/generators/upgrade_open_id_authentication_tables/upgrade_open_id_authentication_tables_generator.rb create mode 100644 test/libs/open_id_authentication/init.rb create mode 100644 test/libs/open_id_authentication/lib/open_id_authentication.rb create mode 100644 test/libs/open_id_authentication/lib/open_id_authentication/association.rb create mode 100644 test/libs/open_id_authentication/lib/open_id_authentication/db_store.rb create mode 100644 test/libs/open_id_authentication/lib/open_id_authentication/mem_cache_store.rb create mode 100644 test/libs/open_id_authentication/lib/open_id_authentication/nonce.rb create mode 100644 test/libs/open_id_authentication/lib/open_id_authentication/request.rb create mode 100644 test/libs/open_id_authentication/lib/open_id_authentication/timeout_fixes.rb create mode 100644 test/libs/open_id_authentication/tasks/open_id_authentication_tasks.rake create mode 100644 test/libs/open_id_authentication/test/mem_cache_store_test.rb create mode 100644 test/libs/open_id_authentication/test/normalize_test.rb create mode 100644 test/libs/open_id_authentication/test/open_id_authentication_test.rb create mode 100644 test/libs/open_id_authentication/test/status_test.rb create mode 100644 test/libs/open_id_authentication/test/test_helper.rb create mode 100644 test/libs/rails_trickery.rb create mode 100644 test/libs/user.rb create mode 100644 test/libs/user_session.rb create mode 100644 test/session_test.rb create mode 100644 test/test_helper.rb diff --git a/CHANGELOG.rdoc b/CHANGELOG.rdoc index e69de29..3e3b071 100644 --- a/CHANGELOG.rdoc +++ b/CHANGELOG.rdoc @@ -0,0 +1,3 @@ +== 1.0.0 + +* Initial release \ No newline at end of file diff --git a/README.rdoc b/README.rdoc index a970b2a..d89756c 100644 --- a/README.rdoc +++ b/README.rdoc @@ -2,8 +2,6 @@ Authlogic OpenID is an extension of the Authlogic library to add OpenID support. Authlogic v2.0 introduced an enhanced API that makes "plugging in" alternate authentication methods as easy as installing a gem. -**DISCLAIMER**: This library is in it's beta stage. It is working properly in my sample application, and I have yet to encounter a problem with it, but I still need to write some tests and document the code. The only reason I released this in its current state is because some people have been waiting to use it. - == Install and use === 1. Make some simple changes to your database: diff --git a/Rakefile b/Rakefile index f818c58..4bbd4eb 100644 --- a/Rakefile +++ b/Rakefile @@ -4,7 +4,7 @@ require "rubygems" require "hoe" require File.dirname(__FILE__) << "/lib/authlogic_openid/version" -Hoe.new("Authlogic OpenID", AuthlogicOpenId::Version::STRING) do |p| +Hoe.new("Authlogic OpenID", AuthlogicOpenid::Version::STRING) do |p| p.name = "authlogic-oid" p.author = "Ben Johnson of Binary Logic" p.email = 'bjohnson@binarylogic.com' @@ -14,6 +14,6 @@ Hoe.new("Authlogic OpenID", AuthlogicOpenId::Version::STRING) do |p| p.history_file = "CHANGELOG.rdoc" p.readme_file = "README.rdoc" p.extra_rdoc_files = ["CHANGELOG.rdoc", "README.rdoc"] - p.test_globs = ["test/*/test_*.rb", "test/*/*_test.rb"] + p.test_globs = ["test/*/test_*.rb", "test/*_test.rb", "test/*/*_test.rb"] p.extra_deps = %w(authlogic) end \ No newline at end of file diff --git a/init.rb b/init.rb index 91026d7..1c92946 100644 --- a/init.rb +++ b/init.rb @@ -1,2 +1 @@ -require "authlogic" -require "authlogic_openid" \ No newline at end of file +require "authlogic_openid" diff --git a/lib/authlogic_openid/acts_as_authentic.rb b/lib/authlogic_openid/acts_as_authentic.rb index 46f1f1d..7702ce4 100644 --- a/lib/authlogic_openid/acts_as_authentic.rb +++ b/lib/authlogic_openid/acts_as_authentic.rb @@ -1,5 +1,11 @@ +# 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 add_acts_as_authentic_module(Methods) @@ -10,6 +16,7 @@ def self.included(klass) end module Methods + # Set up some simple validations def self.included(klass) klass.class_eval do validates_uniqueness_of :openid_identifier, :scope => validations_scope, :if => :using_openid? @@ -17,6 +24,7 @@ def self.included(klass) 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 : OpenIdAuthentication.normalize_identifier(value)) reset_persistence_token if openid_identifier_changed? @@ -24,8 +32,18 @@ def openid_identifier=(value) @openid_error = e.message end - def save(*args, &block) - if !authenticate_with_openid? || (authenticate_with_openid? && authenticate_with_openid) + # 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) + if !perform_validation || !authenticate_with_openid? || (authenticate_with_openid? && authenticate_with_openid) result = super yield(result) if block_given? result @@ -60,7 +78,7 @@ def authenticate_with_openid if result.unsuccessful? @openid_error = result.message else - map_openid_fields(registration) + map_openid_registration(registration) end return true @@ -69,10 +87,10 @@ def authenticate_with_openid return false end - def map_openid_fields(registration) - 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.first_name = registration[:fullname].split(" ").last if respond_to?(:last_name) && !registration[:last_name].blank? + def map_openid_registration(registration) + 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.first_name ||= registration[:fullname].split(" ").last if respond_to?(:last_name) && !registration[:last_name].blank? end def validate_openid diff --git a/lib/authlogic_openid/session.rb b/lib/authlogic_openid/session.rb index dcd9688..7f415da 100644 --- a/lib/authlogic_openid/session.rb +++ b/lib/authlogic_openid/session.rb @@ -1,19 +1,33 @@ 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 - attr_accessor :openid_identifier + 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 : OpenIdAuthentication.normalize_identifier(value) + @openid_error = nil + rescue OpenIdAuthentication::InvalidOpenId => 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? super(&block) @@ -39,5 +53,9 @@ def validate_by_openid end end end + + def validate_openid_error + errors.add(:openid_identifier, @openid_error) if @openid_error + end end end \ No newline at end of file diff --git a/lib/authlogic_openid/version.rb b/lib/authlogic_openid/version.rb index 8a716d3..90ba8fd 100644 --- a/lib/authlogic_openid/version.rb +++ b/lib/authlogic_openid/version.rb @@ -1,11 +1,8 @@ -module AuthlogicOpenid # :nodoc: - # = Version - # +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 @@ -42,15 +39,13 @@ def to_a [@major, @minor, @tiny] end - MAJOR = 0 + MAJOR = 1 MINOR = 0 - TINY = 9 + TINY = 0 # The current version as a Version instance CURRENT = new(MAJOR, MINOR, TINY) # The current version as a String STRING = CURRENT.to_s - end - end \ No newline at end of file diff --git a/test/acts_as_authentic_test.rb b/test/acts_as_authentic_test.rb new file mode 100644 index 0000000..2bacc8d --- /dev/null +++ b/test/acts_as_authentic_test.rb @@ -0,0 +1,90 @@ +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? + 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 = "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? + end + + def test_updating_without_openid + ben = users(:ben) + ben.openid_identifier = nil + ben.password = "test" + ben.password_confirmation = "test" + assert ben.save + assert !redirecting_to_yahoo? + end + + def test_updating_without_validation + ben = users(:ben) + ben.openid_identifier = "https://me.yahoo.com/a/9W0FJjRj0o981TMSs0vqVxPdmMUVOQ--" + assert ben.save(false) + assert !redirecting_to_yahoo? + end +end \ No newline at end of file diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml new file mode 100644 index 0000000..d16c871 --- /dev/null +++ b/test/fixtures/users.yml @@ -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 \ No newline at end of file diff --git a/test/libs/open_id_authentication/CHANGELOG b/test/libs/open_id_authentication/CHANGELOG new file mode 100644 index 0000000..7349bd3 --- /dev/null +++ b/test/libs/open_id_authentication/CHANGELOG @@ -0,0 +1,35 @@ +* 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] \ No newline at end of file diff --git a/test/libs/open_id_authentication/README b/test/libs/open_id_authentication/README new file mode 100644 index 0000000..807cdc7 --- /dev/null +++ b/test/libs/open_id_authentication/README @@ -0,0 +1,231 @@ +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. It also relies on a number of +database tables to store the authentication keys. So you'll have to run the migration to create these before you get started: + + rake open_id_authentication:db:create + +Or, use the included generators to install or upgrade: + + ./script/generate open_id_authentication_tables MigrationName + ./script/generate upgrade_open_id_authentication_tables MigrationName + +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 %> +

+ + <%= text_field_tag "name" %> +

+ +

+ + <%= password_field_tag %> +

+ +

+ ...or use: +

+ +

+ + <%= text_field_tag "openid_identifier" %> +

+ +

+ <%= submit_tag 'Sign in', :disable_with => "Signing in…" %> +

+ <% 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 \ No newline at end of file diff --git a/test/libs/open_id_authentication/Rakefile b/test/libs/open_id_authentication/Rakefile new file mode 100644 index 0000000..31074b8 --- /dev/null +++ b/test/libs/open_id_authentication/Rakefile @@ -0,0 +1,22 @@ +require 'rake' +require 'rake/testtask' +require 'rake/rdoctask' + +desc 'Default: run unit tests.' +task :default => :test + +desc 'Test the open_id_authentication plugin.' +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.pattern = 'test/**/*_test.rb' + t.verbose = true +end + +desc 'Generate documentation for the open_id_authentication plugin.' +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.rdoc_dir = 'rdoc' + rdoc.title = 'OpenIdAuthentication' + rdoc.options << '--line-numbers' << '--inline-source' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/test/libs/open_id_authentication/generators/open_id_authentication_tables/open_id_authentication_tables_generator.rb b/test/libs/open_id_authentication/generators/open_id_authentication_tables/open_id_authentication_tables_generator.rb new file mode 100644 index 0000000..6f78afc --- /dev/null +++ b/test/libs/open_id_authentication/generators/open_id_authentication_tables/open_id_authentication_tables_generator.rb @@ -0,0 +1,11 @@ +class OpenIdAuthenticationTablesGenerator < Rails::Generator::NamedBase + def initialize(runtime_args, runtime_options = {}) + super + end + + def manifest + record do |m| + m.migration_template 'migration.rb', 'db/migrate' + end + end +end diff --git a/test/libs/open_id_authentication/generators/open_id_authentication_tables/templates/migration.rb b/test/libs/open_id_authentication/generators/open_id_authentication_tables/templates/migration.rb new file mode 100644 index 0000000..ef2a0cf --- /dev/null +++ b/test/libs/open_id_authentication/generators/open_id_authentication_tables/templates/migration.rb @@ -0,0 +1,20 @@ +class <%= class_name %> < ActiveRecord::Migration + def self.up + 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 + end + + def self.down + drop_table :open_id_authentication_associations + drop_table :open_id_authentication_nonces + end +end diff --git a/test/libs/open_id_authentication/generators/upgrade_open_id_authentication_tables/templates/migration.rb b/test/libs/open_id_authentication/generators/upgrade_open_id_authentication_tables/templates/migration.rb new file mode 100644 index 0000000..d13bbab --- /dev/null +++ b/test/libs/open_id_authentication/generators/upgrade_open_id_authentication_tables/templates/migration.rb @@ -0,0 +1,26 @@ +class <%= class_name %> < ActiveRecord::Migration + def self.up + drop_table :open_id_authentication_settings + drop_table :open_id_authentication_nonces + + 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 + end + + def self.down + drop_table :open_id_authentication_nonces + + create_table :open_id_authentication_nonces, :force => true do |t| + t.integer :created + t.string :nonce + end + + create_table :open_id_authentication_settings, :force => true do |t| + t.string :setting + t.binary :value + end + end +end diff --git a/test/libs/open_id_authentication/generators/upgrade_open_id_authentication_tables/upgrade_open_id_authentication_tables_generator.rb b/test/libs/open_id_authentication/generators/upgrade_open_id_authentication_tables/upgrade_open_id_authentication_tables_generator.rb new file mode 100644 index 0000000..02fddd7 --- /dev/null +++ b/test/libs/open_id_authentication/generators/upgrade_open_id_authentication_tables/upgrade_open_id_authentication_tables_generator.rb @@ -0,0 +1,11 @@ +class UpgradeOpenIdAuthenticationTablesGenerator < Rails::Generator::NamedBase + def initialize(runtime_args, runtime_options = {}) + super + end + + def manifest + record do |m| + m.migration_template 'migration.rb', 'db/migrate' + end + end +end diff --git a/test/libs/open_id_authentication/init.rb b/test/libs/open_id_authentication/init.rb new file mode 100644 index 0000000..808c7bd --- /dev/null +++ b/test/libs/open_id_authentication/init.rb @@ -0,0 +1,18 @@ +if config.respond_to?(:gems) + config.gem 'ruby-openid', :lib => 'openid', :version => '>=2.0.4' +else + begin + require 'openid' + rescue LoadError + begin + gem 'ruby-openid', '>=2.0.4' + rescue Gem::LoadError + puts "Install the ruby-openid gem to enable OpenID support" + end + end +end + +config.to_prepare do + OpenID::Util.logger = Rails.logger + ActionController::Base.send :include, OpenIdAuthentication +end diff --git a/test/libs/open_id_authentication/lib/open_id_authentication.rb b/test/libs/open_id_authentication/lib/open_id_authentication.rb new file mode 100644 index 0000000..0ba5294 --- /dev/null +++ b/test/libs/open_id_authentication/lib/open_id_authentication.rb @@ -0,0 +1,244 @@ +require 'uri' +require 'openid/extensions/sreg' +require 'openid/extensions/ax' +require 'openid/store/filesystem' + +require File.dirname(__FILE__) + '/open_id_authentication/association' +require File.dirname(__FILE__) + '/open_id_authentication/nonce' +require File.dirname(__FILE__) + '/open_id_authentication/db_store' +require File.dirname(__FILE__) + '/open_id_authentication/mem_cache_store' +require File.dirname(__FILE__) + '/open_id_authentication/request' +require File.dirname(__FILE__) + '/open_id_authentication/timeout_fixes' if OpenID::VERSION == "2.0.4" + +module OpenIdAuthentication + OPEN_ID_AUTHENTICATION_DIR = RAILS_ROOT + "/tmp/openids" + + def self.store + @@store + end + + def self.store=(*store_option) + store, *parameters = *([ store_option ].flatten) + + @@store = case store + when :db + OpenIdAuthentication::DbStore.new + when :mem_cache + OpenIdAuthentication::MemCacheStore.new(*parameters) + when :file + OpenID::Store::Filesystem.new(OPEN_ID_AUTHENTICATION_DIR) + else + raise "Unknown store: #{store}" + end + end + + self.store = :db + + class InvalidOpenId < StandardError + end + + 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 + + # normalizes an OpenID according to http://openid.net/specs/openid-authentication-2_0.html#normalization + def self.normalize_identifier(identifier) + # clean up whitespace + identifier = identifier.to_s.strip + + # if an XRI has a prefix, strip it. + identifier.gsub!(/xri:\/\//i, '') + + # dodge XRIs -- TODO: validate, don't just skip. + unless ['=', '@', '+', '$', '!', '('].include?(identifier.at(0)) + # does it begin with http? if not, add it. + identifier = "http://#{identifier}" unless identifier =~ /^http/i + + # strip any fragments + identifier.gsub!(/\#(.*)$/, '') + + begin + uri = URI.parse(identifier) + uri.scheme = uri.scheme.downcase # URI should do this + identifier = uri.normalize.to_s + rescue URI::InvalidURIError + raise InvalidOpenId.new("#{identifier} is not an OpenID identifier") + end + end + + return identifier + end + + # deprecated for OpenID 2.0, where not all OpenIDs are URLs + def self.normalize_url(url) + ActiveSupport::Deprecation.warn "normalize_url has been deprecated, use normalize_identifier instead" + self.normalize_identifier(url) + end + + protected + def normalize_url(url) + OpenIdAuthentication.normalize_url(url) + end + + def normalize_identifier(url) + OpenIdAuthentication.normalize_identifier(url) + end + + # 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?(identity_url = nil) #:doc: + identity_url ||= params[:openid_identifier] || params[:openid_url] + !identity_url.blank? || params[:open_id_complete] + end + + def authenticate_with_open_id(identity_url = nil, options = {}, &block) #:doc: + identity_url ||= params[:openid_identifier] || params[:openid_url] + + if params[:open_id_complete].nil? + begin_open_id_authentication(identity_url, options, &block) + else + complete_open_id_authentication(&block) + end + end + + private + def begin_open_id_authentication(identity_url, options = {}) + identity_url = normalize_identifier(identity_url) + return_to = options.delete(:return_to) + method = options.delete(:method) + + options[:required] ||= [] # reduces validation later + options[:optional] ||= [] + + open_id_request = open_id_consumer.begin(identity_url) + add_simple_registration_fields(open_id_request, options) + add_ax_fields(open_id_request, options) + + redirect_to(open_id_redirect_url(open_id_request, return_to, method)) + rescue OpenIdAuthentication::InvalidOpenId => e + yield Result[:invalid], identity_url, nil + rescue OpenID::OpenIDError, Timeout::Error => e + logger.error("[OPENID] #{e}") + yield Result[:missing], identity_url, nil + end + + def complete_open_id_authentication + params_with_path = params.reject { |key, value| request.path_parameters[key] } + params_with_path.delete(:format) + open_id_response = timeout_protection_from_identity_server { open_id_consumer.complete(params_with_path, requested_url) } + identity_url = normalize_identifier(open_id_response.display_identifier) if open_id_response.display_identifier + + case open_id_response.status + when OpenID::Consumer::SUCCESS + profile_data = {} + + # merge the SReg data and the AX data into a single hash of profile data + [ OpenID::SReg::Response, OpenID::AX::FetchResponse ].each do |data_response| + if data_response.from_success_response( open_id_response ) + profile_data.merge! data_response.from_success_response( open_id_response ).data + end + end + + yield Result[:successful], identity_url, profile_data + when OpenID::Consumer::CANCEL + yield Result[:canceled], identity_url, nil + when OpenID::Consumer::FAILURE + yield Result[:failed], identity_url, nil + when OpenID::Consumer::SETUP_NEEDED + yield Result[:setup_needed], open_id_response.setup_url, nil + end + end + + def open_id_consumer + OpenID::Consumer.new(session, OpenIdAuthentication.store) + end + + def add_simple_registration_fields(open_id_request, fields) + sreg_request = OpenID::SReg::Request.new + + # filter out AX identifiers (URIs) + required_fields = fields[:required].collect { |f| f.to_s unless f =~ /^https?:\/\// }.compact + optional_fields = fields[:optional].collect { |f| f.to_s unless f =~ /^https?:\/\// }.compact + + sreg_request.request_fields(required_fields, true) unless required_fields.blank? + sreg_request.request_fields(optional_fields, false) unless optional_fields.blank? + sreg_request.policy_url = fields[:policy_url] if fields[:policy_url] + open_id_request.add_extension(sreg_request) + end + + def add_ax_fields( open_id_request, fields ) + ax_request = OpenID::AX::FetchRequest.new + + # look through the :required and :optional fields for URIs (AX identifiers) + fields[:required].each do |f| + next unless f =~ /^https?:\/\// + ax_request.add( OpenID::AX::AttrInfo.new( f, nil, true ) ) + end + + fields[:optional].each do |f| + next unless f =~ /^https?:\/\// + ax_request.add( OpenID::AX::AttrInfo.new( f, nil, false ) ) + end + + open_id_request.add_extension( ax_request ) + end + + def open_id_redirect_url(open_id_request, return_to = nil, method = nil) + open_id_request.return_to_args['_method'] = (method || request.method).to_s + open_id_request.return_to_args['open_id_complete'] = '1' + open_id_request.redirect_url(root_url, return_to || requested_url) + end + + def requested_url + relative_url_root = self.class.respond_to?(:relative_url_root) ? + self.class.relative_url_root.to_s : + request.relative_url_root + "#{request.protocol}#{request.host_with_port}#{ActionController::Base.relative_url_root}#{request.path}" + end + + def timeout_protection_from_identity_server + yield + rescue Timeout::Error + Class.new do + def status + OpenID::FAILURE + end + + def msg + "Identity server timed out" + end + end.new + end +end diff --git a/test/libs/open_id_authentication/lib/open_id_authentication/association.rb b/test/libs/open_id_authentication/lib/open_id_authentication/association.rb new file mode 100644 index 0000000..9654eae --- /dev/null +++ b/test/libs/open_id_authentication/lib/open_id_authentication/association.rb @@ -0,0 +1,9 @@ +module OpenIdAuthentication + class Association < ActiveRecord::Base + set_table_name :open_id_authentication_associations + + def from_record + OpenID::Association.new(handle, secret, issued, lifetime, assoc_type) + end + end +end diff --git a/test/libs/open_id_authentication/lib/open_id_authentication/db_store.rb b/test/libs/open_id_authentication/lib/open_id_authentication/db_store.rb new file mode 100644 index 0000000..780fb6a --- /dev/null +++ b/test/libs/open_id_authentication/lib/open_id_authentication/db_store.rb @@ -0,0 +1,55 @@ +require 'openid/store/interface' + +module OpenIdAuthentication + class DbStore < OpenID::Store::Interface + def self.cleanup_nonces + now = Time.now.to_i + Nonce.delete_all(["timestamp > ? OR timestamp < ?", now + OpenID::Nonce.skew, now - OpenID::Nonce.skew]) + end + + def self.cleanup_associations + now = Time.now.to_i + Association.delete_all(['issued + lifetime > ?',now]) + end + + def store_association(server_url, assoc) + remove_association(server_url, assoc.handle) + Association.create(:server_url => server_url, + :handle => assoc.handle, + :secret => assoc.secret, + :issued => assoc.issued, + :lifetime => assoc.lifetime, + :assoc_type => assoc.assoc_type) + end + + def get_association(server_url, handle = nil) + assocs = if handle.blank? + Association.find_all_by_server_url(server_url) + else + Association.find_all_by_server_url_and_handle(server_url, handle) + end + + assocs.reverse.each do |assoc| + a = assoc.from_record + if a.expires_in == 0 + assoc.destroy + else + return a + end + end if assocs.any? + + return nil + end + + def remove_association(server_url, handle) + Association.delete_all(['server_url = ? AND handle = ?', server_url, handle]) > 0 + end + + def use_nonce(server_url, timestamp, salt) + return false if Nonce.find_by_server_url_and_timestamp_and_salt(server_url, timestamp, salt) + return false if (timestamp - Time.now.to_i).abs > OpenID::Nonce.skew + Nonce.create(:server_url => server_url, :timestamp => timestamp, :salt => salt) + return true + end + end +end diff --git a/test/libs/open_id_authentication/lib/open_id_authentication/mem_cache_store.rb b/test/libs/open_id_authentication/lib/open_id_authentication/mem_cache_store.rb new file mode 100644 index 0000000..b520e4a --- /dev/null +++ b/test/libs/open_id_authentication/lib/open_id_authentication/mem_cache_store.rb @@ -0,0 +1,73 @@ +require 'digest/sha1' +require 'openid/store/interface' + +module OpenIdAuthentication + class MemCacheStore < OpenID::Store::Interface + def initialize(*addresses) + @connection = ActiveSupport::Cache::MemCacheStore.new(addresses) + end + + def store_association(server_url, assoc) + server_key = association_server_key(server_url) + assoc_key = association_key(server_url, assoc.handle) + + assocs = @connection.read(server_key) || {} + assocs[assoc.issued] = assoc_key + + @connection.write(server_key, assocs) + @connection.write(assoc_key, assoc, :expires_in => assoc.lifetime) + end + + def get_association(server_url, handle = nil) + if handle + @connection.read(association_key(server_url, handle)) + else + server_key = association_server_key(server_url) + assocs = @connection.read(server_key) + return if assocs.nil? + + last_key = assocs[assocs.keys.sort.last] + @connection.read(last_key) + end + end + + def remove_association(server_url, handle) + server_key = association_server_key(server_url) + assoc_key = association_key(server_url, handle) + assocs = @connection.read(server_key) + + return false unless assocs && assocs.has_value?(assoc_key) + + assocs = assocs.delete_if { |key, value| value == assoc_key } + + @connection.write(server_key, assocs) + @connection.delete(assoc_key) + + return true + end + + def use_nonce(server_url, timestamp, salt) + return false if @connection.read(nonce_key(server_url, salt)) + return false if (timestamp - Time.now.to_i).abs > OpenID::Nonce.skew + @connection.write(nonce_key(server_url, salt), timestamp, :expires_in => OpenID::Nonce.skew) + return true + end + + private + def association_key(server_url, handle = nil) + "openid_association_#{digest(server_url)}_#{digest(handle)}" + end + + def association_server_key(server_url) + "openid_association_server_#{digest(server_url)}" + end + + def nonce_key(server_url, salt) + "openid_nonce_#{digest(server_url)}_#{digest(salt)}" + end + + def digest(text) + Digest::SHA1.hexdigest(text) + end + end +end diff --git a/test/libs/open_id_authentication/lib/open_id_authentication/nonce.rb b/test/libs/open_id_authentication/lib/open_id_authentication/nonce.rb new file mode 100644 index 0000000..c52f6c5 --- /dev/null +++ b/test/libs/open_id_authentication/lib/open_id_authentication/nonce.rb @@ -0,0 +1,5 @@ +module OpenIdAuthentication + class Nonce < ActiveRecord::Base + set_table_name :open_id_authentication_nonces + end +end diff --git a/test/libs/open_id_authentication/lib/open_id_authentication/request.rb b/test/libs/open_id_authentication/lib/open_id_authentication/request.rb new file mode 100644 index 0000000..e0cc8e3 --- /dev/null +++ b/test/libs/open_id_authentication/lib/open_id_authentication/request.rb @@ -0,0 +1,23 @@ +module OpenIdAuthentication + module Request + def self.included(base) + base.alias_method_chain :request_method, :openid + end + + def request_method_with_openid + if !parameters[:_method].blank? && parameters[:open_id_complete] == '1' + parameters[:_method].to_sym + else + request_method_without_openid + end + end + end +end + +# In Rails 2.3, the request object has been renamed +# from AbstractRequest to Request +if defined? ActionController::Request + ActionController::Request.send :include, OpenIdAuthentication::Request +else + ActionController::AbstractRequest.send :include, OpenIdAuthentication::Request +end diff --git a/test/libs/open_id_authentication/lib/open_id_authentication/timeout_fixes.rb b/test/libs/open_id_authentication/lib/open_id_authentication/timeout_fixes.rb new file mode 100644 index 0000000..cc711c9 --- /dev/null +++ b/test/libs/open_id_authentication/lib/open_id_authentication/timeout_fixes.rb @@ -0,0 +1,20 @@ +# http://trac.openidenabled.com/trac/ticket/156 +module OpenID + @@timeout_threshold = 20 + + def self.timeout_threshold + @@timeout_threshold + end + + def self.timeout_threshold=(value) + @@timeout_threshold = value + end + + class StandardFetcher + def make_http(uri) + http = @proxy.new(uri.host, uri.port) + http.read_timeout = http.open_timeout = OpenID.timeout_threshold + http + end + end +end \ No newline at end of file diff --git a/test/libs/open_id_authentication/tasks/open_id_authentication_tasks.rake b/test/libs/open_id_authentication/tasks/open_id_authentication_tasks.rake new file mode 100644 index 0000000..c71434a --- /dev/null +++ b/test/libs/open_id_authentication/tasks/open_id_authentication_tasks.rake @@ -0,0 +1,30 @@ +namespace :open_id_authentication do + namespace :db do + desc "Creates authentication tables for use with OpenIdAuthentication" + task :create => :environment do + generate_migration(["open_id_authentication_tables", "add_open_id_authentication_tables"]) + end + + desc "Upgrade authentication tables from ruby-openid 1.x.x to 2.x.x" + task :upgrade => :environment do + generate_migration(["upgrade_open_id_authentication_tables", "upgrade_open_id_authentication_tables"]) + end + + def generate_migration(args) + require 'rails_generator' + require 'rails_generator/scripts/generate' + + if ActiveRecord::Base.connection.supports_migrations? + Rails::Generator::Scripts::Generate.new.run(args) + else + raise "Task unavailable to this database (no migration support)" + end + end + + desc "Clear the authentication tables" + task :clear => :environment do + OpenIdAuthentication::DbStore.cleanup_nonces + OpenIdAuthentication::DbStore.cleanup_associations + end + end +end diff --git a/test/libs/open_id_authentication/test/mem_cache_store_test.rb b/test/libs/open_id_authentication/test/mem_cache_store_test.rb new file mode 100644 index 0000000..18a9439 --- /dev/null +++ b/test/libs/open_id_authentication/test/mem_cache_store_test.rb @@ -0,0 +1,151 @@ +require File.dirname(__FILE__) + '/test_helper' +require File.dirname(__FILE__) + '/../lib/open_id_authentication/mem_cache_store' + +# Mock MemCacheStore with MemoryStore for testing +class OpenIdAuthentication::MemCacheStore < OpenID::Store::Interface + def initialize(*addresses) + @connection = ActiveSupport::Cache::MemoryStore.new + end +end + +class MemCacheStoreTest < Test::Unit::TestCase + ALLOWED_HANDLE = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' + + def setup + @store = OpenIdAuthentication::MemCacheStore.new + end + + def test_store + server_url = "http://www.myopenid.com/openid" + assoc = gen_assoc(0) + + # Make sure that a missing association returns no result + assert_retrieve(server_url) + + # Check that after storage, getting returns the same result + @store.store_association(server_url, assoc) + assert_retrieve(server_url, nil, assoc) + + # more than once + assert_retrieve(server_url, nil, assoc) + + # Storing more than once has no ill effect + @store.store_association(server_url, assoc) + assert_retrieve(server_url, nil, assoc) + + # Removing an association that does not exist returns not present + assert_remove(server_url, assoc.handle + 'x', false) + + # Removing an association that does not exist returns not present + assert_remove(server_url + 'x', assoc.handle, false) + + # Removing an association that is present returns present + assert_remove(server_url, assoc.handle, true) + + # but not present on subsequent calls + assert_remove(server_url, assoc.handle, false) + + # Put assoc back in the store + @store.store_association(server_url, assoc) + + # More recent and expires after assoc + assoc2 = gen_assoc(1) + @store.store_association(server_url, assoc2) + + # After storing an association with a different handle, but the + # same server_url, the handle with the later expiration is returned. + assert_retrieve(server_url, nil, assoc2) + + # We can still retrieve the older association + assert_retrieve(server_url, assoc.handle, assoc) + + # Plus we can retrieve the association with the later expiration + # explicitly + assert_retrieve(server_url, assoc2.handle, assoc2) + + # More recent, and expires earlier than assoc2 or assoc. Make sure + # that we're picking the one with the latest issued date and not + # taking into account the expiration. + assoc3 = gen_assoc(2, 100) + @store.store_association(server_url, assoc3) + + assert_retrieve(server_url, nil, assoc3) + assert_retrieve(server_url, assoc.handle, assoc) + assert_retrieve(server_url, assoc2.handle, assoc2) + assert_retrieve(server_url, assoc3.handle, assoc3) + + assert_remove(server_url, assoc2.handle, true) + + assert_retrieve(server_url, nil, assoc3) + assert_retrieve(server_url, assoc.handle, assoc) + assert_retrieve(server_url, assoc2.handle, nil) + assert_retrieve(server_url, assoc3.handle, assoc3) + + assert_remove(server_url, assoc2.handle, false) + assert_remove(server_url, assoc3.handle, true) + + assert_retrieve(server_url, nil, assoc) + assert_retrieve(server_url, assoc.handle, assoc) + assert_retrieve(server_url, assoc2.handle, nil) + assert_retrieve(server_url, assoc3.handle, nil) + + assert_remove(server_url, assoc2.handle, false) + assert_remove(server_url, assoc.handle, true) + assert_remove(server_url, assoc3.handle, false) + + assert_retrieve(server_url, nil, nil) + assert_retrieve(server_url, assoc.handle, nil) + assert_retrieve(server_url, assoc2.handle, nil) + assert_retrieve(server_url, assoc3.handle, nil) + + assert_remove(server_url, assoc2.handle, false) + assert_remove(server_url, assoc.handle, false) + assert_remove(server_url, assoc3.handle, false) + end + + def test_nonce + server_url = "http://www.myopenid.com/openid" + + [server_url, ''].each do |url| + nonce1 = OpenID::Nonce::mk_nonce + + assert_nonce(nonce1, true, url, "#{url}: nonce allowed by default") + assert_nonce(nonce1, false, url, "#{url}: nonce not allowed twice") + assert_nonce(nonce1, false, url, "#{url}: nonce not allowed third time") + + # old nonces shouldn't pass + old_nonce = OpenID::Nonce::mk_nonce(3600) + assert_nonce(old_nonce, false, url, "Old nonce #{old_nonce.inspect} passed") + end + end + + private + def gen_assoc(issued, lifetime = 600) + secret = OpenID::CryptUtil.random_string(20, nil) + handle = OpenID::CryptUtil.random_string(128, ALLOWED_HANDLE) + OpenID::Association.new(handle, secret, Time.now + issued, lifetime, 'HMAC-SHA1') + end + + def assert_retrieve(url, handle = nil, expected = nil) + assoc = @store.get_association(url, handle) + + if expected.nil? + assert_nil(assoc) + else + assert_equal(expected, assoc) + assert_equal(expected.handle, assoc.handle) + assert_equal(expected.secret, assoc.secret) + end + end + + def assert_remove(url, handle, expected) + present = @store.remove_association(url, handle) + assert_equal(expected, present) + end + + def assert_nonce(nonce, expected, server_url, msg = "") + stamp, salt = OpenID::Nonce::split_nonce(nonce) + actual = @store.use_nonce(server_url, stamp, salt) + assert_equal(expected, actual, msg) + end +end diff --git a/test/libs/open_id_authentication/test/normalize_test.rb b/test/libs/open_id_authentication/test/normalize_test.rb new file mode 100644 index 0000000..635d3ab --- /dev/null +++ b/test/libs/open_id_authentication/test/normalize_test.rb @@ -0,0 +1,32 @@ +require File.dirname(__FILE__) + '/test_helper' + +class NormalizeTest < Test::Unit::TestCase + include OpenIdAuthentication + + NORMALIZATIONS = { + "openid.aol.com/nextangler" => "http://openid.aol.com/nextangler", + "http://openid.aol.com/nextangler" => "http://openid.aol.com/nextangler", + "https://openid.aol.com/nextangler" => "https://openid.aol.com/nextangler", + "HTTP://OPENID.AOL.COM/NEXTANGLER" => "http://openid.aol.com/NEXTANGLER", + "HTTPS://OPENID.AOL.COM/NEXTANGLER" => "https://openid.aol.com/NEXTANGLER", + "loudthinking.com" => "http://loudthinking.com/", + "http://loudthinking.com" => "http://loudthinking.com/", + "http://loudthinking.com:80" => "http://loudthinking.com/", + "https://loudthinking.com:443" => "https://loudthinking.com/", + "http://loudthinking.com:8080" => "http://loudthinking.com:8080/", + "techno-weenie.net" => "http://techno-weenie.net/", + "http://techno-weenie.net" => "http://techno-weenie.net/", + "http://techno-weenie.net " => "http://techno-weenie.net/", + "=name" => "=name" + } + + def test_normalizations + NORMALIZATIONS.each do |from, to| + assert_equal to, normalize_identifier(from) + end + end + + def test_broken_open_id + assert_raises(InvalidOpenId) { normalize_identifier(nil) } + end +end diff --git a/test/libs/open_id_authentication/test/open_id_authentication_test.rb b/test/libs/open_id_authentication/test/open_id_authentication_test.rb new file mode 100644 index 0000000..ddcc17b --- /dev/null +++ b/test/libs/open_id_authentication/test/open_id_authentication_test.rb @@ -0,0 +1,46 @@ +require File.dirname(__FILE__) + '/test_helper' + +class OpenIdAuthenticationTest < Test::Unit::TestCase + def setup + @controller = Class.new do + include OpenIdAuthentication + def params() {} end + end.new + end + + def test_authentication_should_fail_when_the_identity_server_is_missing + open_id_consumer = mock() + open_id_consumer.expects(:begin).raises(OpenID::OpenIDError) + @controller.expects(:open_id_consumer).returns(open_id_consumer) + @controller.expects(:logger).returns(mock(:error => true)) + + @controller.send(:authenticate_with_open_id, "http://someone.example.com") do |result, identity_url| + assert result.missing? + assert_equal "Sorry, the OpenID server couldn't be found", result.message + end + end + + def test_authentication_should_be_invalid_when_the_identity_url_is_invalid + @controller.send(:authenticate_with_open_id, "!") do |result, identity_url| + assert result.invalid?, "Result expected to be invalid but was not" + assert_equal "Sorry, but this does not appear to be a valid OpenID", result.message + end + end + + def test_authentication_should_fail_when_the_identity_server_times_out + open_id_consumer = mock() + open_id_consumer.expects(:begin).raises(Timeout::Error, "Identity Server took too long.") + @controller.expects(:open_id_consumer).returns(open_id_consumer) + @controller.expects(:logger).returns(mock(:error => true)) + + @controller.send(:authenticate_with_open_id, "http://someone.example.com") do |result, identity_url| + assert result.missing? + assert_equal "Sorry, the OpenID server couldn't be found", result.message + end + end + + def test_authentication_should_begin_when_the_identity_server_is_present + @controller.expects(:begin_open_id_authentication) + @controller.send(:authenticate_with_open_id, "http://someone.example.com") + end +end diff --git a/test/libs/open_id_authentication/test/status_test.rb b/test/libs/open_id_authentication/test/status_test.rb new file mode 100644 index 0000000..b1d5e09 --- /dev/null +++ b/test/libs/open_id_authentication/test/status_test.rb @@ -0,0 +1,14 @@ +require File.dirname(__FILE__) + '/test_helper' + +class StatusTest < Test::Unit::TestCase + include OpenIdAuthentication + + def test_state_conditional + assert Result[:missing].missing? + assert Result[:missing].unsuccessful? + assert !Result[:missing].successful? + + assert Result[:successful].successful? + assert !Result[:successful].unsuccessful? + end +end \ No newline at end of file diff --git a/test/libs/open_id_authentication/test/test_helper.rb b/test/libs/open_id_authentication/test/test_helper.rb new file mode 100644 index 0000000..43216e1 --- /dev/null +++ b/test/libs/open_id_authentication/test/test_helper.rb @@ -0,0 +1,17 @@ +require 'test/unit' +require 'rubygems' + +gem 'activesupport' +require 'active_support' + +gem 'actionpack' +require 'action_controller' + +gem 'mocha' +require 'mocha' + +gem 'ruby-openid' +require 'openid' + +RAILS_ROOT = File.dirname(__FILE__) unless defined? RAILS_ROOT +require File.dirname(__FILE__) + "/../lib/open_id_authentication" diff --git a/test/libs/rails_trickery.rb b/test/libs/rails_trickery.rb new file mode 100644 index 0000000..186d39b --- /dev/null +++ b/test/libs/rails_trickery.rb @@ -0,0 +1,41 @@ +# The only reason I am doing all of this non sense is becuase the openid_authentication requires that +# these constants be present. The only other alternative is to use an entire rails application for testing +# which is a little too overboard for this, I think. + +RAILS_ROOT = '' + +class ActionController < Authlogic::TestCase::MockController + class Request < Authlogic::TestCase::MockRequest + def request_method + "" + end + end + + def root_url + '' + end + + def request + return @request if defined?(@request) + super + # Rails does some crazy s#!t with the "method" method. If I don't do this I get a "wrong arguments (0 for 1) error" + @request.class.class_eval do + def method + nil + end + end + @request + end + + def url_for(*args) + '' + end + + def redirecting_to + @redirect_to + end + + def redirect_to(*args) + @redirect_to = args + end +end \ No newline at end of file diff --git a/test/libs/user.rb b/test/libs/user.rb new file mode 100644 index 0000000..c043f96 --- /dev/null +++ b/test/libs/user.rb @@ -0,0 +1,3 @@ +class User < ActiveRecord::Base + acts_as_authentic +end \ No newline at end of file diff --git a/test/libs/user_session.rb b/test/libs/user_session.rb new file mode 100644 index 0000000..8c19d19 --- /dev/null +++ b/test/libs/user_session.rb @@ -0,0 +1,2 @@ +class UserSession < Authlogic::Session::Base +end \ No newline at end of file diff --git a/test/session_test.rb b/test/session_test.rb new file mode 100644 index 0000000..a123399 --- /dev/null +++ b/test/session_test.rb @@ -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 = "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 !redirecting_to_yahoo? + 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? + end +end \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..a987d46 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,78 @@ +require "test/unit" +require "rubygems" +require "ruby-debug" +require "active_record" + +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" +require File.dirname(__FILE__) + "/../../authlogic/lib/authlogic" +require File.dirname(__FILE__) + "/../../authlogic/lib/authlogic/test_case" +require File.dirname(__FILE__) + "/libs/rails_trickery" +require File.dirname(__FILE__) + '/libs/open_id_authentication/lib/open_id_authentication' +ActionController.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' + +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 activate_authlogic + Authlogic::Session::Base.controller = controller + end + + def controller + @controller ||= Authlogic::TestCase::ControllerAdapter.new(ActionController.new) + end + + def redirecting_to_yahoo? + controller.redirecting_to.to_s =~ /^https:\/\/open.login.yahooapis.com\/openid\/op\/auth/ + end +end \ No newline at end of file