--- /dev/null
+OpenIdAuthentication.store = :file
+* Dump heavy lifting off to rack-openid gem. OpenIdAuthentication is just a simple controller concern.
+
* Fake HTTP method from OpenID server since they only support a GET. Eliminates the need to set an extra route to match the server's reply. [Josh Peek]
* OpenID 2.0 recommends that forms should use the field name "openid_identifier" rather than "openid_url" [Josh Peek]
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
+OpenID authentication uses the session, so be sure that you haven't turned that off.
Alternatively, you can use the file-based store, which just relies on on tmp/openids being present in RAILS_ROOT. But be aware that this store only works if you have a single application server. And it's not safe to use across NFS. It's recommended that you use the database store if at all possible. To use the file-based store, you'll also have to add this line to your config/environment.rb:
OpenIdAuthentication.store = :file
This particular plugin also relies on the fact that the authentication action allows for both POST and GET operations.
-If you're using RESTful authentication, you'll need to explicitly allow for this in your routes.rb.
+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:
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.
end
end
end
-
-
+
+
private
def successful_login
session[:user_id] = @current_user.id
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,
+ authenticate_with_open_id(identity_url,
:required => [ :nickname, :email ],
:optional => :fullname) do |result, identity_url, registration|
case result.status
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)
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,
+ 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'
+++ /dev/null
-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
+++ /dev/null
-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
+++ /dev/null
-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
+++ /dev/null
-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
+++ /dev/null
-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
-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
+if Rails.version < '3'
+ config.gem 'rack-openid', :lib => 'rack/openid', :version => '>=0.2.1'
end
-config.to_prepare do
+require 'open_id_authentication'
+
+config.middleware.use OpenIdAuthentication
+
+config.after_initialize do
OpenID::Util.logger = Rails.logger
ActionController::Base.send :include, OpenIdAuthentication
end
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/request'
-require File.dirname(__FILE__) + '/open_id_authentication/timeout_fixes' if OpenID::VERSION == "2.0.4"
+require 'openid'
+require 'rack/openid'
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
@@store
store, *parameters = *([ store_option ].flatten)
@@store = case store
- when :db
- OpenIdAuthentication::DbStore.new
+ when :memory
+ require 'openid/store/memory'
+ OpenID::Store::Memory.new
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
store
end
end
- self.store = :db
-
- class InvalidOpenId < StandardError
- end
+ self.store = nil
class Result
ERROR_MESSAGES = {
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)
+ # The parameter name of "openid_identifier" is used rather than
+ # the Rails convention "open_id_identifier" because that's what
+ # the specification dictates in order to get browser auto-complete
+ # working across sites
+ def using_open_id?(identifier = nil) #:doc:
+ identifier ||= open_id_identifier
+ !identifier.blank? || request.env[Rack::OpenID::RESPONSE]
end
- # 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]
+ def authenticate_with_open_id(identifier = nil, options = {}, &block) #:doc:
+ identifier ||= open_id_identifier
- if params[:open_id_complete].nil?
- begin_open_id_authentication(identity_url, options, &block)
- else
+ if request.env[Rack::OpenID::RESPONSE]
complete_open_id_authentication(&block)
+ else
+ begin_open_id_authentication(identifier, options, &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
+ def open_id_identifier
+ params[:openid_identifier] || params[:openid_url]
+ end
+
+ def begin_open_id_authentication(identifier, options = {})
+ options[:identifier] = identifier
+ value = Rack::OpenID.build_header(options)
+ response.headers[Rack::OpenID::AUTHENTICATE_HEADER] = value
+ head :unauthorized
end
def complete_open_id_authentication
- 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
+ response = request.env[Rack::OpenID::RESPONSE]
+ identifier = response.display_identifier
- case open_id_response.status
+ case 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
+ yield Result[:successful], identifier,
+ OpenID::SReg::Response.from_success_response(response)
+ when :missing
+ yield Result[:missing], identifier, nil
+ when :invalid
+ yield Result[:invalid], identifier, nil
when OpenID::Consumer::CANCEL
- yield Result[:canceled], identity_url, nil
+ yield Result[:canceled], identifier, nil
when OpenID::Consumer::FAILURE
- yield Result[:failed], identity_url, nil
+ yield Result[:failed], identifier, nil
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
-
- 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
+++ /dev/null
-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
+++ /dev/null
-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
+++ /dev/null
-module OpenIdAuthentication
- class Nonce < ActiveRecord::Base
- set_table_name :open_id_authentication_nonces
- end
-end
+++ /dev/null
-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
+++ /dev/null
-# 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
+++ /dev/null
-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
+++ /dev/null
-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
+++ /dev/null
-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
+++ /dev/null
-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
+++ /dev/null
-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"