##// END OF EJS Templates
Link to watched issues list on my page....
Link to watched issues list on my page. git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@2457 e93f8b46-1217-0410-a6f0-8f06a7374b81

File last commit:

r2376:f70be197e0ae
r2396:5bdd4291624c
Show More
associationmanager.rb
340 lines | 12.6 KiB | text/x-ruby | RubyLexer
require "openid/dh"
require "openid/util"
require "openid/kvpost"
require "openid/cryptutil"
require "openid/protocolerror"
require "openid/association"
module OpenID
class Consumer
# A superclass for implementing Diffie-Hellman association sessions.
class DiffieHellmanSession
class << self
attr_reader :session_type, :secret_size, :allowed_assoc_types,
:hashfunc
end
def initialize(dh=nil)
if dh.nil?
dh = DiffieHellman.from_defaults
end
@dh = dh
end
# Return the query parameters for requesting an association
# using this Diffie-Hellman association session
def get_request
args = {'dh_consumer_public' => CryptUtil.num_to_base64(@dh.public)}
if (!@dh.using_default_values?)
args['dh_modulus'] = CryptUtil.num_to_base64(@dh.modulus)
args['dh_gen'] = CryptUtil.num_to_base64(@dh.generator)
end
return args
end
# Process the response from a successful association request and
# return the shared secret for this association
def extract_secret(response)
dh_server_public64 = response.get_arg(OPENID_NS, 'dh_server_public',
NO_DEFAULT)
enc_mac_key64 = response.get_arg(OPENID_NS, 'enc_mac_key', NO_DEFAULT)
dh_server_public = CryptUtil.base64_to_num(dh_server_public64)
enc_mac_key = Util.from_base64(enc_mac_key64)
return @dh.xor_secret(self.class.hashfunc,
dh_server_public, enc_mac_key)
end
end
# A Diffie-Hellman association session that uses SHA1 as its hash
# function
class DiffieHellmanSHA1Session < DiffieHellmanSession
@session_type = 'DH-SHA1'
@secret_size = 20
@allowed_assoc_types = ['HMAC-SHA1']
@hashfunc = CryptUtil.method(:sha1)
end
# A Diffie-Hellman association session that uses SHA256 as its hash
# function
class DiffieHellmanSHA256Session < DiffieHellmanSession
@session_type = 'DH-SHA256'
@secret_size = 32
@allowed_assoc_types = ['HMAC-SHA256']
@hashfunc = CryptUtil.method(:sha256)
end
# An association session that does not use encryption
class NoEncryptionSession
class << self
attr_reader :session_type, :allowed_assoc_types
end
@session_type = 'no-encryption'
@allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256']
def get_request
return {}
end
def extract_secret(response)
mac_key64 = response.get_arg(OPENID_NS, 'mac_key', NO_DEFAULT)
return Util.from_base64(mac_key64)
end
end
# An object that manages creating and storing associations for an
# OpenID provider endpoint
class AssociationManager
def self.create_session(session_type)
case session_type
when 'no-encryption'
NoEncryptionSession.new
when 'DH-SHA1'
DiffieHellmanSHA1Session.new
when 'DH-SHA256'
DiffieHellmanSHA256Session.new
else
raise ArgumentError, "Unknown association session type: "\
"#{session_type.inspect}"
end
end
def initialize(store, server_url, compatibility_mode=false,
negotiator=nil)
@store = store
@server_url = server_url
@compatibility_mode = compatibility_mode
@negotiator = negotiator || DefaultNegotiator
end
def get_association
if @store.nil?
return nil
end
assoc = @store.get_association(@server_url)
if assoc.nil? || assoc.expires_in <= 0
assoc = negotiate_association
if !assoc.nil?
@store.store_association(@server_url, assoc)
end
end
return assoc
end
def negotiate_association
assoc_type, session_type = @negotiator.get_allowed_type
begin
return request_association(assoc_type, session_type)
rescue ServerError => why
supported_types = extract_supported_association_type(why, assoc_type)
if !supported_types.nil?
# Attempt to create an association from the assoc_type and
# session_type that the server told us it supported.
assoc_type, session_type = supported_types
begin
return request_association(assoc_type, session_type)
rescue ServerError => why
Util.log("Server #{@server_url} refused its suggested " \
"association type: session_type=#{session_type}, " \
"assoc_type=#{assoc_type}")
return nil
end
end
end
end
protected
def extract_supported_association_type(server_error, assoc_type)
# Any error message whose code is not 'unsupported-type' should
# be considered a total failure.
if (server_error.error_code != 'unsupported-type' or
server_error.message.is_openid1)
Util.log("Server error when requesting an association from "\
"#{@server_url}: #{server_error.error_text}")
return nil
end
# The server didn't like the association/session type that we
# sent, and it sent us back a message that might tell us how to
# handle it.
Util.log("Unsupported association type #{assoc_type}: "\
"#{server_error.error_text}")
# Extract the session_type and assoc_type from the error message
assoc_type = server_error.message.get_arg(OPENID_NS, 'assoc_type')
session_type = server_error.message.get_arg(OPENID_NS, 'session_type')
if assoc_type.nil? or session_type.nil?
Util.log("Server #{@server_url} responded with unsupported "\
"association session but did not supply a fallback.")
return nil
elsif !@negotiator.allowed?(assoc_type, session_type)
Util.log("Server sent unsupported session/association type: "\
"session_type=#{session_type}, assoc_type=#{assoc_type}")
return nil
else
return [assoc_type, session_type]
end
end
# Make and process one association request to this endpoint's OP
# endpoint URL. Returns an association object or nil if the
# association processing failed. Raises ServerError when the
# remote OpenID server returns an error.
def request_association(assoc_type, session_type)
assoc_session, args = create_associate_request(assoc_type, session_type)
begin
response = OpenID.make_kv_post(args, @server_url)
return extract_association(response, assoc_session)
rescue HTTPStatusError => why
Util.log("Got HTTP status error when requesting association: #{why}")
return nil
rescue Message::KeyNotFound => why
Util.log("Missing required parameter in response from "\
"#{@server_url}: #{why}")
return nil
rescue ProtocolError => why
Util.log("Protocol error processing response from #{@server_url}: "\
"#{why}")
return nil
end
end
# Create an association request for the given assoc_type and
# session_type. Returns a pair of the association session object
# and the request message that will be sent to the server.
def create_associate_request(assoc_type, session_type)
assoc_session = self.class.create_session(session_type)
args = {
'mode' => 'associate',
'assoc_type' => assoc_type,
}
if !@compatibility_mode
args['ns'] = OPENID2_NS
end
# Leave out the session type if we're in compatibility mode
# *and* it's no-encryption.
if !@compatibility_mode ||
assoc_session.class.session_type != 'no-encryption'
args['session_type'] = assoc_session.class.session_type
end
args.merge!(assoc_session.get_request)
message = Message.from_openid_args(args)
return assoc_session, message
end
# Given an association response message, extract the OpenID 1.X
# session type. Returns the association type for this message
#
# This function mostly takes care of the 'no-encryption' default
# behavior in OpenID 1.
#
# If the association type is plain-text, this function will
# return 'no-encryption'
def get_openid1_session_type(assoc_response)
# If it's an OpenID 1 message, allow session_type to default
# to nil (which signifies "no-encryption")
session_type = assoc_response.get_arg(OPENID1_NS, 'session_type')
# Handle the differences between no-encryption association
# respones in OpenID 1 and 2:
# no-encryption is not really a valid session type for
# OpenID 1, but we'll accept it anyway, while issuing a
# warning.
if session_type == 'no-encryption'
Util.log("WARNING: #{@server_url} sent 'no-encryption'"\
"for OpenID 1.X")
# Missing or empty session type is the way to flag a
# 'no-encryption' response. Change the session type to
# 'no-encryption' so that it can be handled in the same
# way as OpenID 2 'no-encryption' respones.
elsif session_type == '' || session_type.nil?
session_type = 'no-encryption'
end
return session_type
end
def self.extract_expires_in(message)
# expires_in should be a base-10 string.
expires_in_str = message.get_arg(OPENID_NS, 'expires_in', NO_DEFAULT)
if !(/\A\d+\Z/ =~ expires_in_str)
raise ProtocolError, "Invalid expires_in field: #{expires_in_str}"
end
expires_in_str.to_i
end
# Attempt to extract an association from the response, given the
# association response message and the established association
# session.
def extract_association(assoc_response, assoc_session)
# Extract the common fields from the response, raising an
# exception if they are not found
assoc_type = assoc_response.get_arg(OPENID_NS, 'assoc_type',
NO_DEFAULT)
assoc_handle = assoc_response.get_arg(OPENID_NS, 'assoc_handle',
NO_DEFAULT)
expires_in = self.class.extract_expires_in(assoc_response)
# OpenID 1 has funny association session behaviour.
if assoc_response.is_openid1
session_type = get_openid1_session_type(assoc_response)
else
session_type = assoc_response.get_arg(OPENID2_NS, 'session_type',
NO_DEFAULT)
end
# Session type mismatch
if assoc_session.class.session_type != session_type
if (assoc_response.is_openid1 and session_type == 'no-encryption')
# In OpenID 1, any association request can result in a
# 'no-encryption' association response. Setting
# assoc_session to a new no-encryption session should
# make the rest of this function work properly for
# that case.
assoc_session = NoEncryptionSession.new
else
# Any other mismatch, regardless of protocol version
# results in the failure of the association session
# altogether.
raise ProtocolError, "Session type mismatch. Expected "\
"#{assoc_session.class.session_type}, got "\
"#{session_type}"
end
end
# Make sure assoc_type is valid for session_type
if !assoc_session.class.allowed_assoc_types.member?(assoc_type)
raise ProtocolError, "Unsupported assoc_type for session "\
"#{assoc_session.class.session_type} "\
"returned: #{assoc_type}"
end
# Delegate to the association session to extract the secret
# from the response, however is appropriate for that session
# type.
begin
secret = assoc_session.extract_secret(assoc_response)
rescue Message::KeyNotFound, ArgumentError => why
raise ProtocolError, "Malformed response for "\
"#{assoc_session.class.session_type} "\
"session: #{why.message}"
end
return Association.from_expires_in(expires_in, assoc_handle, secret,
assoc_type)
end
end
end
end