# This module is responsible for adding RPX functionality to Authlogic. Checkout the README for more info and please
# see the sub modules for detailed documentation.
module AuthlogicRpx
# This module is responsible for adding in the RPX 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
# RPX is being used.
def self.included(klass)
klass.class_eval do
extend Config
add_acts_as_authentic_module(Methods, :prepend)
end
end
class GeneralError < StandardError
end
class ConfigurationError < StandardError
end
module Config
# account_merge_enabled is used to enable merging of accounts.
#
# * Default: false
# * Accepts: boolean
def account_merge_enabled(value=false)
account_merge_enabled_value(value)
end
def account_merge_enabled_value(value=nil)
rw_config(:account_merge_enabled,value,false)
end
alias_method :account_merge_enabled=,:account_merge_enabled
# account_mapping_mode is used to explicitly set/override the mapping behaviour.
#
# * Default: :auto
# * Accepts: :auto, :none, :internal, :rpxnow
def account_mapping_mode(value=:auto)
account_mapping_mode_value(value)
end
def account_mapping_mode_value(value=nil)
raise AuthlogicRpx::ActsAsAuthentic::ConfigurationError.new unless value.nil? || [:auto,:none,:internal].include?( value )
rw_config(:account_mapping_mode,value,:auto)
end
alias_method :account_mapping_mode=,:account_mapping_mode
# returns the actual account mapping mode in use - resolves :auto to actual mechanism
#
attr_writer :account_mapping_mode_used
def account_mapping_mode_used
@account_mapping_mode_used ||= (
account_mapping_mode_value == :auto ?
( RPXIdentifier.table_exists? ?
:internal :
( self.column_names.include?("rpx_identifier") ? :none : AuthlogicRpx::ActsAsAuthentic::ConfigurationError.new )
) :
account_mapping_mode_value
)
end
# determines if no account mapping is supported (the only behaviour in authlogic_rpx v1.0.4)
def using_no_mapping?
account_mapping_mode_used == :none
end
# determines if internal account mapping is enabled (behaviour added in authlogic_rpx v1.1.0)
def using_internal_mapping?
account_mapping_mode_used == :internal
end
# determines if rpxnow account mapping is enabled (currently not implemented)
def using_rpx_mapping?
account_mapping_mode_used == :rpxnow
end
end
module Methods
# Mix-in the required methods based on mapping mode
#
def self.included(klass)
klass.class_eval do
case
when using_no_mapping?
include AuthlogicRpx::MethodSet_NoMapping
when using_internal_mapping?
include AuthlogicRpx::MethodSet_InternalMapping
has_many :rpx_identifiers, :class_name => 'RPXIdentifier', :dependent => :destroy
# Add custom find_by_rpx_identifier class method
#
def self.find_by_rpx_identifier(id)
identifier = RPXIdentifier.find_by_identifier(id)
if identifier.nil?
if self.column_names.include? 'rpx_identifier'
# check for authentication using <=1.0.4, migrate identifier to rpx_identifiers table
user = self.find( :first, :conditions => [ "rpx_identifier = ?", id ] )
unless user.nil?
user.add_rpx_identifier( id, 'Unknown' )
end
return user
else
return nil
end
else
identifier.send( self.methods.include?(:class_name) ? self.class_name.downcase : self.to_s.classify.downcase )
end
end
else
raise AuthlogicRpx::ActsAsAuthentic::ConfigurationError.new( "invalid or unsupported account_mapping_mode" )
end
# Set up some fundamental conditional validations
validates_length_of_password_field_options validates_length_of_password_field_options.merge(:if => :validate_password_not_rpx?)
validates_confirmation_of_password_field_options validates_confirmation_of_password_field_options.merge(:if => :validate_password_not_rpx?)
validates_length_of_password_confirmation_field_options validates_length_of_password_confirmation_field_options.merge(:if => :validate_password_not_rpx?)
before_validation :adding_rpx_identifier
end
# add relations and validation to RPXIdentifier based on the actual user model class name used
#
RPXIdentifier.class_eval do
belongs_to klass.name.downcase.to_sym
validates_presence_of "#{klass.name.downcase}_id".to_sym
end
end
# test if account it using normal password authentication
def using_password?
!send(crypted_password_field).blank?
end
private
# tests if password authentication should be checked instead of rpx (i.e. if rpx is enabled but not used by this user)
def validate_password_not_rpx?
!using_rpx? && require_password?
end
# determines if account merging is enabled; delegates to class method
def account_merge_enabled?
self.class.account_merge_enabled_value
end
# hook for adding RPX identifier to an existing account. This is invoked prior to model validation.
# RPX information is plucked from the controller session object (where it was placed by the session model as a result
# of the RPX callback)
# The minimal action taken is to add an RPXIdentifier object to the user.
#
# This procedure chains to the map_added_rpx_data, which may be over-ridden in your project to perform
# additional mapping of RPX information to the user model as may be desired.
#
def adding_rpx_identifier
return true unless session_class && session_class.controller
added_rpx_data = session_class.controller.session['added_rpx_data']
unless added_rpx_data.blank?
session_class.controller.session['added_rpx_data'] = nil
rpx_id = added_rpx_data['profile']['identifier']
rpx_provider_name = added_rpx_data['profile']['providerName']
unless self.identified_by?( rpx_id )
# identifier not already set for this user..
another_user = self.class.find_by_rpx_identifier( rpx_id )
if another_user
return false unless account_merge_enabled?
# another user already has this id registered..
# merge all IDs from another_user to self, with application callbacks before/after
before_merge_rpx_data( another_user, self )
merge_user_id another_user
after_merge_rpx_data( another_user, self )
else
self.add_rpx_identifier( rpx_id, rpx_provider_name, added_rpx_data)
end
end
map_added_rpx_data( added_rpx_data )
end
end
# map_added_rpx_data maps additional fields from the RPX response into the user object during the "add RPX to existing account" process.
# Override this in your user model to perform field mapping as may be desired
# See https://rpxnow.com/docs#profile_data for the definition of available attributes
#
# "self" at this point is the user model. Map details as appropriate from the rpx_data structure provided.
#
def map_added_rpx_data( rpx_data )
end
# before_merge_rpx_data provides a hook for application developers to perform data migration prior to the merging of user accounts.
# This method is called just before authlogic_rpx merges the user registration for 'from_user' into 'to_user'
# Authlogic_RPX is responsible for merging registration data.
#
# By default, it does not merge any other details (e.g. application data ownership)
#
def before_merge_rpx_data( from_user, to_user )
end
# after_merge_rpx_data provides a hook for application developers to perform account clean-up after authlogic_rpx has
# migrated registration details.
#
# By default, does nothing. It could, for example, be used to delete or disable the 'from_user' account
#
def after_merge_rpx_data( from_user, to_user )
end
end
end
# Mix-in collection of methods that are specific to no-mapping mode of operation
#
module MethodSet_NoMapping
# test if account it using RPX authentication
#
def using_rpx?
!rpx_identifier.blank?
end
# adds RPX identification to the instance.
# Abstracts how the RPX identifier is added to allow for multiplicity of underlying implementations
#
def add_rpx_identifier( rpx_id, rpx_provider_name, rpx_data = {})
self.rpx_identifier = rpx_id
#TODO: make rpx_provider_name a std param?
end
# Checks if given identifier is an identity for this account
#
def identified_by?( id )
self.rpx_identifier == id
end
# merge_user_id is an internal method used to merge the actual RPX identifiers
#
def merge_user_id( from_user )
self.rpx_identifier = from_user.rpx_identifier
from_user.rpx_identifier = nil
from_user.save
from_user.reload
end
# Uses default find_by_rpx_identifier class method
# Add an rpx_identifier collection method
def rpx_identifiers
[{ :identifier => rpx_identifier, :provider_name => "Unknown" }]
end
end
# Mix-in collection of methods that are specific to internal mapping mode of operation
#
module MethodSet_InternalMapping
# test if account it using RPX authentication
#
def using_rpx?
!rpx_identifiers.empty?
end
# adds RPX identification to the instance.
# Abstracts how the RPX identifier is added to allow for multiplicity of underlying implementations
#
def add_rpx_identifier( rpx_id, rpx_provider_name, rpx_data = {} )
self.rpx_identifiers.build(:identifier => rpx_id, :provider_name => rpx_provider_name, :rpx_data => rpx_data )
end
# Checks if given identifier is an identity for this account
#
def identified_by?( id )
self.rpx_identifiers.find_by_identifier( id )
end
# merge_user_id is an internal method used to merge the actual RPX identifiers
#
def merge_user_id( from_user )
self.rpx_identifiers << from_user.rpx_identifiers
from_user.reload
end
end
end