##// 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
idres.rb
523 lines | 18.4 KiB | text/x-ruby | RubyLexer
require "openid/message"
require "openid/protocolerror"
require "openid/kvpost"
require "openid/consumer/discovery"
require "openid/urinorm"
module OpenID
class TypeURIMismatch < ProtocolError
attr_reader :type_uri, :endpoint
def initialize(type_uri, endpoint)
@type_uri = type_uri
@endpoint = endpoint
end
end
class Consumer
@openid1_return_to_nonce_name = 'rp_nonce'
@openid1_return_to_claimed_id_name = 'openid1_claimed_id'
# Set the name of the query parameter that this library will use
# to thread a nonce through an OpenID 1 transaction. It will be
# appended to the return_to URL.
def self.openid1_return_to_nonce_name=(query_arg_name)
@openid1_return_to_nonce_name = query_arg_name
end
# See openid1_return_to_nonce_name= documentation
def self.openid1_return_to_nonce_name
@openid1_return_to_nonce_name
end
# Set the name of the query parameter that this library will use
# to thread the requested URL through an OpenID 1 transaction (for
# use when verifying discovered information). It will be appended
# to the return_to URL.
def self.openid1_return_to_claimed_id_name=(query_arg_name)
@openid1_return_to_claimed_id_name = query_arg_name
end
# See openid1_return_to_claimed_id_name=
def self.openid1_return_to_claimed_id_name
@openid1_return_to_claimed_id_name
end
# Handles an openid.mode=id_res response. This object is
# instantiated and used by the Consumer.
class IdResHandler
attr_reader :endpoint, :message
def initialize(message, current_url, store=nil, endpoint=nil)
@store = store # Fer the nonce and invalidate_handle
@message = message
@endpoint = endpoint
@current_url = current_url
@signed_list = nil
# Start the verification process
id_res
end
def signed_fields
signed_list.map {|x| 'openid.' + x}
end
protected
# This method will raise ProtocolError unless the request is a
# valid id_res response. Once it has been verified, the methods
# 'endpoint', 'message', and 'signed_fields' contain the
# verified information.
def id_res
check_for_fields
verify_return_to
verify_discovery_results
check_signature
check_nonce
end
def server_url
@endpoint.nil? ? nil : @endpoint.server_url
end
def openid_namespace
@message.get_openid_namespace
end
def fetch(field, default=NO_DEFAULT)
@message.get_arg(OPENID_NS, field, default)
end
def signed_list
if @signed_list.nil?
signed_list_str = fetch('signed', nil)
if signed_list_str.nil?
raise ProtocolError, 'Response missing signed list'
end
@signed_list = signed_list_str.split(',', -1)
end
@signed_list
end
def check_for_fields
# XXX: if a field is missing, we should not have to explicitly
# check that it's present, just make sure that the fields are
# actually being used by the rest of the code in
# tests. Although, which fields are signed does need to be
# checked somewhere.
basic_fields = ['return_to', 'assoc_handle', 'sig', 'signed']
basic_sig_fields = ['return_to', 'identity']
case openid_namespace
when OPENID2_NS
require_fields = basic_fields + ['op_endpoint']
require_sigs = basic_sig_fields +
['response_nonce', 'claimed_id', 'assoc_handle',]
when OPENID1_NS
require_fields = basic_fields + ['identity']
require_sigs = basic_sig_fields
else
raise RuntimeError, "check_for_fields doesn't know about "\
"namespace #{openid_namespace.inspect}"
end
require_fields.each do |field|
if !@message.has_key?(OPENID_NS, field)
raise ProtocolError, "Missing required field #{field}"
end
end
require_sigs.each do |field|
# Field is present and not in signed list
if @message.has_key?(OPENID_NS, field) && !signed_list.member?(field)
raise ProtocolError, "#{field.inspect} not signed"
end
end
end
def verify_return_to
begin
msg_return_to = URI.parse(URINorm::urinorm(fetch('return_to')))
rescue URI::InvalidURIError
raise ProtocolError, ("return_to is not a valid URI")
end
verify_return_to_args(msg_return_to)
if !@current_url.nil?
verify_return_to_base(msg_return_to)
end
end
def verify_return_to_args(msg_return_to)
return_to_parsed_query = {}
if !msg_return_to.query.nil?
CGI.parse(msg_return_to.query).each_pair do |k, vs|
return_to_parsed_query[k] = vs[0]
end
end
query = @message.to_post_args
return_to_parsed_query.each_pair do |rt_key, rt_val|
msg_val = query[rt_key]
if msg_val.nil?
raise ProtocolError, "Message missing return_to argument '#{rt_key}'"
elsif msg_val != rt_val
raise ProtocolError, ("Parameter '#{rt_key}' value "\
"#{msg_val.inspect} does not match "\
"return_to's value #{rt_val.inspect}")
end
end
@message.get_args(BARE_NS).each_pair do |bare_key, bare_val|
rt_val = return_to_parsed_query[bare_key]
if not return_to_parsed_query.has_key? bare_key
# This may be caused by your web framework throwing extra
# entries in to your parameters hash that were not GET or
# POST parameters. For example, Rails has been known to
# add "controller" and "action" keys; another server adds
# at least a "format" key.
raise ProtocolError, ("Unexpected parameter (not on return_to): "\
"'#{bare_key}'=#{rt_val.inspect})")
end
if rt_val != bare_val
raise ProtocolError, ("Parameter '#{bare_key}' value "\
"#{bare_val.inspect} does not match "\
"return_to's value #{rt_val.inspect}")
end
end
end
def verify_return_to_base(msg_return_to)
begin
app_parsed = URI.parse(URINorm::urinorm(@current_url))
rescue URI::InvalidURIError
raise ProtocolError, "current_url is not a valid URI: #{@current_url}"
end
[:scheme, :host, :port, :path].each do |meth|
if msg_return_to.send(meth) != app_parsed.send(meth)
raise ProtocolError, "return_to #{meth.to_s} does not match"
end
end
end
# Raises ProtocolError if the signature is bad
def check_signature
if @store.nil?
assoc = nil
else
assoc = @store.get_association(server_url, fetch('assoc_handle'))
end
if assoc.nil?
check_auth
else
if assoc.expires_in <= 0
# XXX: It might be a good idea sometimes to re-start the
# authentication with a new association. Doing it
# automatically opens the possibility for
# denial-of-service by a server that just returns expired
# associations (or really short-lived associations)
raise ProtocolError, "Association with #{server_url} expired"
elsif !assoc.check_message_signature(@message)
raise ProtocolError, "Bad signature in response from #{server_url}"
end
end
end
def check_auth
Util.log("Using 'check_authentication' with #{server_url}")
begin
request = create_check_auth_request
rescue Message::KeyNotFound => why
raise ProtocolError, "Could not generate 'check_authentication' "\
"request: #{why.message}"
end
response = OpenID.make_kv_post(request, server_url)
process_check_auth_response(response)
end
def create_check_auth_request
signed_list = @message.get_arg(OPENID_NS, 'signed', NO_DEFAULT).split(',')
# check that we got all the signed arguments
signed_list.each {|k|
@message.get_aliased_arg(k, NO_DEFAULT)
}
ca_message = @message.copy
ca_message.set_arg(OPENID_NS, 'mode', 'check_authentication')
return ca_message
end
# Process the response message from a check_authentication
# request, invalidating associations if requested.
def process_check_auth_response(response)
is_valid = response.get_arg(OPENID_NS, 'is_valid', 'false')
invalidate_handle = response.get_arg(OPENID_NS, 'invalidate_handle')
if !invalidate_handle.nil?
Util.log("Received 'invalidate_handle' from server #{server_url}")
if @store.nil?
Util.log('Unexpectedly got "invalidate_handle" without a store!')
else
@store.remove_association(server_url, invalidate_handle)
end
end
if is_valid != 'true'
raise ProtocolError, ("Server #{server_url} responds that the "\
"'check_authentication' call is not valid")
end
end
def check_nonce
case openid_namespace
when OPENID1_NS
nonce =
@message.get_arg(BARE_NS, Consumer.openid1_return_to_nonce_name)
# We generated the nonce, so it uses the empty string as the
# server URL
server_url = ''
when OPENID2_NS
nonce = @message.get_arg(OPENID2_NS, 'response_nonce')
server_url = self.server_url
else
raise StandardError, 'Not reached'
end
if nonce.nil?
raise ProtocolError, 'Nonce missing from response'
end
begin
time, extra = Nonce.split_nonce(nonce)
rescue ArgumentError => why
raise ProtocolError, "Malformed nonce: #{nonce.inspect}"
end
if !@store.nil? && !@store.use_nonce(server_url, time, extra)
raise ProtocolError, ("Nonce already used or out of range: "\
"#{nonce.inspect}")
end
end
def verify_discovery_results
begin
case openid_namespace
when OPENID1_NS
verify_discovery_results_openid1
when OPENID2_NS
verify_discovery_results_openid2
else
raise StandardError, "Not reached: #{openid_namespace}"
end
rescue Message::KeyNotFound => why
raise ProtocolError, "Missing required field: #{why.message}"
end
end
def verify_discovery_results_openid2
to_match = OpenIDServiceEndpoint.new
to_match.type_uris = [OPENID_2_0_TYPE]
to_match.claimed_id = fetch('claimed_id', nil)
to_match.local_id = fetch('identity', nil)
to_match.server_url = fetch('op_endpoint')
if to_match.claimed_id.nil? && !to_match.local_id.nil?
raise ProtocolError, ('openid.identity is present without '\
'openid.claimed_id')
elsif !to_match.claimed_id.nil? && to_match.local_id.nil?
raise ProtocolError, ('openid.claimed_id is present without '\
'openid.identity')
# This is a response without identifiers, so there's really no
# checking that we can do, so return an endpoint that's for
# the specified `openid.op_endpoint'
elsif to_match.claimed_id.nil?
@endpoint =
OpenIDServiceEndpoint.from_op_endpoint_url(to_match.server_url)
return
end
if @endpoint.nil?
Util.log('No pre-discovered information supplied')
discover_and_verify(to_match.claimed_id, [to_match])
else
begin
verify_discovery_single(@endpoint, to_match)
rescue ProtocolError => why
Util.log("Error attempting to use stored discovery "\
"information: #{why.message}")
Util.log("Attempting discovery to verify endpoint")
discover_and_verify(to_match.claimed_id, [to_match])
end
end
if @endpoint.claimed_id != to_match.claimed_id
@endpoint = @endpoint.dup
@endpoint.claimed_id = to_match.claimed_id
end
end
def verify_discovery_results_openid1
claimed_id =
@message.get_arg(BARE_NS, Consumer.openid1_return_to_claimed_id_name)
if claimed_id.nil?
if @endpoint.nil?
raise ProtocolError, ("When using OpenID 1, the claimed ID must "\
"be supplied, either by passing it through "\
"as a return_to parameter or by using a "\
"session, and supplied to the IdResHandler "\
"when it is constructed.")
else
claimed_id = @endpoint.claimed_id
end
end
to_match = OpenIDServiceEndpoint.new
to_match.type_uris = [OPENID_1_1_TYPE]
to_match.local_id = fetch('identity')
# Restore delegate information from the initiation phase
to_match.claimed_id = claimed_id
to_match_1_0 = to_match.dup
to_match_1_0.type_uris = [OPENID_1_0_TYPE]
if !@endpoint.nil?
begin
begin
verify_discovery_single(@endpoint, to_match)
rescue TypeURIMismatch
verify_discovery_single(@endpoint, to_match_1_0)
end
rescue ProtocolError => why
Util.log('Error attempting to use stored discovery information: ' +
why.message)
Util.log('Attempting discovery to verify endpoint')
else
return @endpoint
end
end
# Either no endpoint was supplied or OpenID 1.x verification
# of the information that's in the message failed on that
# endpoint.
discover_and_verify(to_match.claimed_id, [to_match, to_match_1_0])
end
# Given an endpoint object created from the information in an
# OpenID response, perform discovery and verify the discovery
# results, returning the matching endpoint that is the result of
# doing that discovery.
def discover_and_verify(claimed_id, to_match_endpoints)
Util.log("Performing discovery on #{claimed_id}")
_, services = OpenID.discover(claimed_id)
if services.length == 0
# XXX: this might want to be something other than
# ProtocolError. In Python, it's DiscoveryFailure
raise ProtocolError, ("No OpenID information found at "\
"#{claimed_id}")
end
verify_discovered_services(claimed_id, services, to_match_endpoints)
end
def verify_discovered_services(claimed_id, services, to_match_endpoints)
# Search the services resulting from discovery to find one
# that matches the information from the assertion
failure_messages = []
for endpoint in services
for to_match_endpoint in to_match_endpoints
begin
verify_discovery_single(endpoint, to_match_endpoint)
rescue ProtocolError => why
failure_messages << why.message
else
# It matches, so discover verification has
# succeeded. Return this endpoint.
@endpoint = endpoint
return
end
end
end
Util.log("Discovery verification failure for #{claimed_id}")
failure_messages.each do |failure_message|
Util.log(" * Endpoint mismatch: " + failure_message)
end
# XXX: is DiscoveryFailure in Python OpenID
raise ProtocolError, ("No matching endpoint found after "\
"discovering #{claimed_id}")
end
def verify_discovery_single(endpoint, to_match)
# Every type URI that's in the to_match endpoint has to be
# present in the discovered endpoint.
for type_uri in to_match.type_uris
if !endpoint.uses_extension(type_uri)
raise TypeURIMismatch.new(type_uri, endpoint)
end
end
# Fragments do not influence discovery, so we can't compare a
# claimed identifier with a fragment to discovered information.
defragged_claimed_id =
case Yadis::XRI.identifier_scheme(endpoint.claimed_id)
when :xri
endpoint.claimed_id
when :uri
begin
parsed = URI.parse(endpoint.claimed_id)
rescue URI::InvalidURIError
endpoint.claimed_id
else
parsed.fragment = nil
parsed.to_s
end
else
raise StandardError, 'Not reached'
end
if defragged_claimed_id != endpoint.claimed_id
raise ProtocolError, ("Claimed ID does not match (different "\
"subjects!), Expected "\
"#{defragged_claimed_id}, got "\
"#{endpoint.claimed_id}")
end
if to_match.get_local_id != endpoint.get_local_id
raise ProtocolError, ("local_id mismatch. Expected "\
"#{to_match.get_local_id}, got "\
"#{endpoint.get_local_id}")
end
# If the server URL is nil, this must be an OpenID 1
# response, because op_endpoint is a required parameter in
# OpenID 2. In that case, we don't actually care what the
# discovered server_url is, because signature checking or
# check_auth should take care of that check for us.
if to_match.server_url.nil?
if to_match.preferred_namespace != OPENID1_NS
raise StandardError,
"The code calling this must ensure that OpenID 2 "\
"responses have a non-none `openid.op_endpoint' and "\
"that it is set as the `server_url' attribute of the "\
"`to_match' endpoint."
end
elsif to_match.server_url != endpoint.server_url
raise ProtocolError, ("OP Endpoint mismatch. Expected"\
"#{to_match.server_url}, got "\
"#{endpoint.server_url}")
end
end
end
end
end