require 'openid/extension' require 'openid/util' require 'openid/message' module OpenID module SReg DATA_FIELDS = { 'fullname'=>'Full Name', 'nickname'=>'Nickname', 'dob'=>'Date of Birth', 'email'=>'E-mail Address', 'gender'=>'Gender', 'postcode'=>'Postal Code', 'country'=>'Country', 'language'=>'Language', 'timezone'=>'Time Zone', } NS_URI_1_0 = 'http://openid.net/sreg/1.0' NS_URI_1_1 = 'http://openid.net/extensions/sreg/1.1' NS_URI = NS_URI_1_1 begin Message.register_namespace_alias(NS_URI_1_1, 'sreg') rescue NamespaceAliasRegistrationError => e Util.log(e) end # raise ArgumentError if fieldname is not in the defined sreg fields def OpenID.check_sreg_field_name(fieldname) unless DATA_FIELDS.member? fieldname raise ArgumentError, "#{fieldname} is not a defined simple registration field" end end # Does the given endpoint advertise support for simple registration? def OpenID.supports_sreg?(endpoint) endpoint.uses_extension(NS_URI_1_1) || endpoint.uses_extension(NS_URI_1_0) end # Extract the simple registration namespace URI from the given # OpenID message. Handles OpenID 1 and 2, as well as both sreg # namespace URIs found in the wild, as well as missing namespace # definitions (for OpenID 1) def OpenID.get_sreg_ns(message) [NS_URI_1_1, NS_URI_1_0].each{|ns| if message.namespaces.get_alias(ns) return ns end } # try to add an alias, since we didn't find one ns = NS_URI_1_1 begin message.namespaces.add_alias(ns, 'sreg') rescue IndexError raise NamespaceError end return ns end # The simple registration namespace was not found and could not # be created using the expected name (there's another extension # using the name 'sreg') # # This is not illegal, for OpenID 2, although it probably # indicates a problem, since it's not expected that other extensions # will re-use the alias that is in use for OpenID 1. # # If this is an OpenID 1 request, then there is no recourse. This # should not happen unless some code has modified the namespaces for # the message that is being processed. class NamespaceError < ArgumentError end # An object to hold the state of a simple registration request. class Request < Extension attr_reader :optional, :required, :ns_uri attr_accessor :policy_url def initialize(required = nil, optional = nil, policy_url = nil, ns_uri = NS_URI) super() @policy_url = policy_url @ns_uri = ns_uri @ns_alias = 'sreg' @required = [] @optional = [] if required request_fields(required, true, true) end if optional request_fields(optional, false, true) end end # Create a simple registration request that contains the # fields that were requested in the OpenID request with the # given arguments # Takes an OpenID::CheckIDRequest, returns an OpenID::Sreg::Request # return nil if the extension was not requested. def self.from_openid_request(request) # Since we're going to mess with namespace URI mapping, don't # mutate the object that was passed in. message = request.message.copy ns_uri = OpenID::get_sreg_ns(message) args = message.get_args(ns_uri) return nil if args == {} req = new(nil,nil,nil,ns_uri) req.parse_extension_args(args) return req end # Parse the unqualified simple registration request # parameters and add them to this object. # # This method is essentially the inverse of # getExtensionArgs. This method restores the serialized simple # registration request fields. # # If you are extracting arguments from a standard OpenID # checkid_* request, you probably want to use fromOpenIDRequest, # which will extract the sreg namespace and arguments from the # OpenID request. This method is intended for cases where the # OpenID server needs more control over how the arguments are # parsed than that method provides. def parse_extension_args(args, strict = false) required_items = args['required'] unless required_items.nil? or required_items.empty? required_items.split(',').each{|field_name| begin request_field(field_name, true, strict) rescue ArgumentError raise if strict end } end optional_items = args['optional'] unless optional_items.nil? or optional_items.empty? optional_items.split(',').each{|field_name| begin request_field(field_name, false, strict) rescue ArgumentError raise if strict end } end @policy_url = args['policy_url'] end # A list of all of the simple registration fields that were # requested, whether they were required or optional. def all_requested_fields @required + @optional end # Have any simple registration fields been requested? def were_fields_requested? !all_requested_fields.empty? end # Request the specified field from the OpenID user # field_name: the unqualified simple registration field name # required: whether the given field should be presented # to the user as being a required to successfully complete # the request # strict: whether to raise an exception when a field is # added to a request more than once # Raises ArgumentError if the field_name is not a simple registration # field, or if strict is set and a field is added more than once def request_field(field_name, required=false, strict=false) OpenID::check_sreg_field_name(field_name) if strict if (@required + @optional).member? field_name raise ArgumentError, 'That field has already been requested' end else return if @required.member? field_name if @optional.member? field_name if required @optional.delete field_name else return end end end if required @required << field_name else @optional << field_name end end # Add the given list of fields to the request. def request_fields(field_names, required = false, strict = false) raise ArgumentError unless field_names.respond_to?(:each) and field_names[0].is_a?(String) field_names.each{|fn|request_field(fn, required, strict)} end # Get a hash of unqualified simple registration arguments # representing this request. # This method is essentially the inverse of parse_extension_args. # This method serializes the simple registration request fields. def get_extension_args args = {} args['required'] = @required.join(',') unless @required.empty? args['optional'] = @optional.join(',') unless @optional.empty? args['policy_url'] = @policy_url unless @policy_url.nil? return args end def member?(field_name) all_requested_fields.member?(field_name) end end # Represents the data returned in a simple registration response # inside of an OpenID id_res response. This object will be # created by the OpenID server, added to the id_res response # object, and then extracted from the id_res message by the Consumer. class Response < Extension attr_reader :ns_uri, :data def initialize(data = {}, ns_uri=NS_URI) @ns_alias = 'sreg' @data = data @ns_uri = ns_uri end # Take a Request and a hash of simple registration # values and create a Response object containing that data. def self.extract_response(request, data) arf = request.all_requested_fields resp_data = data.reject{|k,v| !arf.member?(k) || v.nil? } new(resp_data, request.ns_uri) end # Create an Response object from an # OpenID::Consumer::SuccessResponse from consumer.complete # If you set the signed_only parameter to false, unsigned data from # the id_res message from the server will be processed. def self.from_success_response(success_response, signed_only = true) ns_uri = OpenID::get_sreg_ns(success_response.message) if signed_only args = success_response.get_signed_ns(ns_uri) return nil if args.nil? # No signed args, so fail else args = success_response.message.get_args(ns_uri) end args.reject!{|k,v| !DATA_FIELDS.member?(k) } new(args, ns_uri) end # Get the fields to put in the simple registration namespace # when adding them to an id_res message. def get_extension_args return @data end # Read-only hashlike interface. # Raises an exception if the field name is bad def [](field_name) OpenID::check_sreg_field_name(field_name) data[field_name] end def empty? @data.empty? end # XXX is there more to a hashlike interface I should add? end end end