Added open_id_authentication plugin

git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@2438 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Eric Davis 2009-02-11 19:06:45 +00:00
parent f70be197e0
commit 30171f3ab6
21 changed files with 1090 additions and 0 deletions

View File

@ -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]

View File

@ -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 %>
<p>
<label for="name">Username:</label>
<%= text_field_tag "name" %>
</p>
<p>
<label for="password">Password:</label>
<%= password_field_tag %>
</p>
<p>
...or use:
</p>
<p>
<label for="openid_identifier">OpenID:</label>
<%= text_field_tag "openid_identifier" %>
</p>
<p>
<%= submit_tag 'Sign in', :disable_with => "Signing in&hellip;" %>
</p>
<% end %>
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def create
if using_open_id?
open_id_authentication
else
password_authentication(params[:name], params[:password])
end
end
protected
def password_authentication(name, password)
if @current_user = @account.users.authenticate(params[:name], params[:password])
successful_login
else
failed_login "Sorry, that username/password doesn't work"
end
end
def open_id_authentication
authenticate_with_open_id do |result, identity_url|
if result.successful?
if @current_user = @account.users.find_by_identity_url(identity_url)
successful_login
else
failed_login "Sorry, no user by that identity URL exists (#{identity_url})"
end
else
failed_login result.message
end
end
end
private
def successful_login
session[:user_id] = @current_user.id
redirect_to(root_url)
end
def failed_login(message)
flash[:error] = message
redirect_to(new_session_url)
end
end
If you're fine with the result messages above and don't need individual logic on a per-failure basis,
you can collapse the case into a mere boolean:
def open_id_authentication
authenticate_with_open_id do |result, identity_url|
if result.successful? && @current_user = @account.users.find_by_identity_url(identity_url)
successful_login
else
failed_login(result.message || "Sorry, no user by that identity URL exists (#{identity_url})")
end
end
end
Simple Registration OpenID Extension
====================================
Some OpenID Providers support this lightweight profile exchange protocol. See more: http://www.openidenabled.com/openid/simple-registration-extension
You can support it in your app by changing #open_id_authentication
def open_id_authentication(identity_url)
# Pass optional :required and :optional keys to specify what sreg fields you want.
# Be sure to yield registration, a third argument in the #authenticate_with_open_id block.
authenticate_with_open_id(identity_url,
:required => [ :nickname, :email ],
:optional => :fullname) do |result, identity_url, registration|
case result.status
when :missing
failed_login "Sorry, the OpenID server couldn't be found"
when :invalid
failed_login "Sorry, but this does not appear to be a valid OpenID"
when :canceled
failed_login "OpenID verification was canceled"
when :failed
failed_login "Sorry, the OpenID verification failed"
when :successful
if @current_user = @account.users.find_by_identity_url(identity_url)
assign_registration_attributes!(registration)
if current_user.save
successful_login
else
failed_login "Your OpenID profile registration failed: " +
@current_user.errors.full_messages.to_sentence
end
else
failed_login "Sorry, no user by that identity URL exists"
end
end
end
end
# registration is a hash containing the valid sreg keys given above
# use this to map them to fields of your user model
def assign_registration_attributes!(registration)
model_to_registration_mapping.each do |model_attribute, registration_attribute|
unless registration[registration_attribute].blank?
@current_user.send("#{model_attribute}=", registration[registration_attribute])
end
end
end
def model_to_registration_mapping
{ :login => 'nickname', :email => 'email', :display_name => 'fullname' }
end
Attribute Exchange OpenID Extension
===================================
Some OpenID providers also support the OpenID AX (attribute exchange) protocol for exchanging identity information between endpoints. See more: http://openid.net/specs/openid-attribute-exchange-1_0.html
Accessing AX data is very similar to the Simple Registration process, described above -- just add the URI identifier for the AX field to your :optional or :required parameters. For example:
authenticate_with_open_id(identity_url,
:required => [ :email, 'http://schema.openid.net/birthDate' ]) do |result, identity_url, registration|
This would provide the sreg data for :email, and the AX data for 'http://schema.openid.net/birthDate'
Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,241 @@
require 'uri'
require 'openid/extensions/sreg'
require 'openid/extensions/ax'
require 'openid/store/filesystem'
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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
module OpenIdAuthentication
class Nonce < ActiveRecord::Base
set_table_name :open_id_authentication_nonces
end
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"