Update OpenID authentication plugin to latest version

This commit is contained in:
Tom Hughes 2010-05-15 17:39:52 +01:00
parent 9b10cbccb6
commit a108e9290c
20 changed files with 71 additions and 535 deletions

View file

@ -0,0 +1 @@
OpenIdAuthentication.store = :file

View file

@ -1,3 +1,5 @@
* Dump heavy lifting off to rack-openid gem. OpenIdAuthentication is just a simple controller concern.
* Fake HTTP method from OpenID server since they only support a GET. Eliminates the need to set an extra route to match the server's reply. [Josh Peek] * 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] * OpenID 2.0 recommends that forms should use the field name "openid_identifier" rather than "openid_url" [Josh Peek]

View file

@ -14,15 +14,7 @@ The specification used is http://openid.net/specs/openid-authentication-2_0.html
Prerequisites Prerequisites
============= =============
OpenID authentication uses the session, so be sure that you haven't turned that off. It also relies on a number of OpenID authentication uses the session, so be sure that you haven't turned that off.
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: 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:

View file

@ -1,22 +0,0 @@
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

@ -1,11 +0,0 @@
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

@ -1,20 +0,0 @@
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

@ -1,26 +0,0 @@
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

@ -1,11 +0,0 @@
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

@ -1,18 +1,12 @@
if config.respond_to?(:gems) if Rails.version < '3'
config.gem 'ruby-openid', :lib => 'openid', :version => '>=2.0.4' config.gem 'rack-openid', :lib => 'rack/openid', :version => '>=0.2.1'
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 end
config.to_prepare do require 'open_id_authentication'
config.middleware.use OpenIdAuthentication
config.after_initialize do
OpenID::Util.logger = Rails.logger OpenID::Util.logger = Rails.logger
ActionController::Base.send :include, OpenIdAuthentication ActionController::Base.send :include, OpenIdAuthentication
end end

View file

@ -1,16 +1,16 @@
require 'uri' require 'uri'
require 'openid/extensions/sreg' require 'openid'
require 'openid/extensions/ax' require 'rack/openid'
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/request'
require File.dirname(__FILE__) + '/open_id_authentication/timeout_fixes' if OpenID::VERSION == "2.0.4"
module OpenIdAuthentication module OpenIdAuthentication
OPEN_ID_AUTHENTICATION_DIR = RAILS_ROOT + "/tmp/openids" def self.new(app)
store = OpenIdAuthentication.store
if store.nil?
Rails.logger.warn "OpenIdAuthentication.store is nil. Using in-memory store."
end
::Rack::OpenID.new(app, OpenIdAuthentication.store)
end
def self.store def self.store
@@store @@store
@ -20,19 +20,22 @@ module OpenIdAuthentication
store, *parameters = *([ store_option ].flatten) store, *parameters = *([ store_option ].flatten)
@@store = case store @@store = case store
when :db when :memory
OpenIdAuthentication::DbStore.new require 'openid/store/memory'
OpenID::Store::Memory.new
when :file when :file
OpenID::Store::Filesystem.new(OPEN_ID_AUTHENTICATION_DIR) require 'openid/store/filesystem'
OpenID::Store::Filesystem.new(Rails.root.join('tmp/openids'))
when :memcache
require 'memcache'
require 'openid/store/memcache'
OpenID::Store::Memcache.new(MemCache.new(parameters))
else else
store store
end end
end end
self.store = :db self.store = nil
class InvalidOpenId < StandardError
end
class Result class Result
ERROR_MESSAGES = { ERROR_MESSAGES = {
@ -70,171 +73,56 @@ module OpenIdAuthentication
end end
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 protected
def normalize_url(url) # The parameter name of "openid_identifier" is used rather than
OpenIdAuthentication.normalize_url(url) # the Rails convention "open_id_identifier" because that's what
# the specification dictates in order to get browser auto-complete
# working across sites
def using_open_id?(identifier = nil) #:doc:
identifier ||= open_id_identifier
!identifier.blank? || request.env[Rack::OpenID::RESPONSE]
end end
def normalize_identifier(url) def authenticate_with_open_id(identifier = nil, options = {}, &block) #:doc:
OpenIdAuthentication.normalize_identifier(url) identifier ||= open_id_identifier
end
# The parameter name of "openid_identifier" is used rather than the Rails convention "open_id_identifier" if request.env[Rack::OpenID::RESPONSE]
# 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) complete_open_id_authentication(&block)
else
begin_open_id_authentication(identifier, options, &block)
end end
end end
private private
def begin_open_id_authentication(identity_url, options = {}) def open_id_identifier
identity_url = normalize_identifier(identity_url) params[:openid_identifier] || params[:openid_url]
return_to = options.delete(:return_to) end
method = options.delete(:method)
options[:required] ||= [] # reduces validation later def begin_open_id_authentication(identifier, options = {})
options[:optional] ||= [] options[:identifier] = identifier
value = Rack::OpenID.build_header(options)
open_id_request = open_id_consumer.begin(identity_url) response.headers[Rack::OpenID::AUTHENTICATE_HEADER] = value
add_simple_registration_fields(open_id_request, options) head :unauthorized
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 end
def complete_open_id_authentication def complete_open_id_authentication
params_with_path = params.reject { |key, value| request.path_parameters[key] } response = request.env[Rack::OpenID::RESPONSE]
params_with_path.delete(:format) identifier = response.display_identifier
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 case response.status
when OpenID::Consumer::SUCCESS when OpenID::Consumer::SUCCESS
profile_data = {} yield Result[:successful], identifier,
OpenID::SReg::Response.from_success_response(response)
# merge the SReg data and the AX data into a single hash of profile data when :missing
[ OpenID::SReg::Response, OpenID::AX::FetchResponse ].each do |data_response| yield Result[:missing], identifier, nil
if data_response.from_success_response( open_id_response ) when :invalid
profile_data.merge! data_response.from_success_response( open_id_response ).data yield Result[:invalid], identifier, nil
end
end
yield Result[:successful], identity_url, profile_data
when OpenID::Consumer::CANCEL when OpenID::Consumer::CANCEL
yield Result[:canceled], identity_url, nil yield Result[:canceled], identifier, nil
when OpenID::Consumer::FAILURE when OpenID::Consumer::FAILURE
yield Result[:failed], identity_url, nil yield Result[:failed], identifier, nil
when OpenID::Consumer::SETUP_NEEDED when OpenID::Consumer::SETUP_NEEDED
yield Result[:setup_needed], open_id_response.setup_url, nil yield Result[:setup_needed], response.setup_url, nil
end end
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 end

View file

@ -1,9 +0,0 @@
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

@ -1,55 +0,0 @@
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

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

View file

@ -1,23 +0,0 @@
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

@ -1,20 +0,0 @@
# 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

@ -1,30 +0,0 @@
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

@ -1,32 +0,0 @@
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

@ -1,46 +0,0 @@
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

@ -1,14 +0,0 @@
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

@ -1,17 +0,0 @@
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"