git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@2437 e93f8b46-1217-0410-a6f0-8f06a7374b81
554 lines
16 KiB
Ruby
554 lines
16 KiB
Ruby
require 'openid/util'
|
|
require 'openid/kvform'
|
|
|
|
module OpenID
|
|
|
|
IDENTIFIER_SELECT = 'http://specs.openid.net/auth/2.0/identifier_select'
|
|
|
|
# URI for Simple Registration extension, the only commonly deployed
|
|
# OpenID 1.x extension, and so a special case.
|
|
SREG_URI = 'http://openid.net/sreg/1.0'
|
|
|
|
# The OpenID 1.x namespace URIs
|
|
OPENID1_NS = 'http://openid.net/signon/1.0'
|
|
OPENID11_NS = 'http://openid.net/signon/1.1'
|
|
OPENID1_NAMESPACES = [OPENID1_NS, OPENID11_NS]
|
|
|
|
# The OpenID 2.0 namespace URI
|
|
OPENID2_NS = 'http://specs.openid.net/auth/2.0'
|
|
|
|
# The namespace consisting of pairs with keys that are prefixed with
|
|
# "openid." but not in another namespace.
|
|
NULL_NAMESPACE = :null_namespace
|
|
|
|
# The null namespace, when it is an allowed OpenID namespace
|
|
OPENID_NS = :openid_namespace
|
|
|
|
# The top-level namespace, excluding all pairs with keys that start
|
|
# with "openid."
|
|
BARE_NS = :bare_namespace
|
|
|
|
# Limit, in bytes, of identity provider and return_to URLs,
|
|
# including response payload. See OpenID 1.1 specification,
|
|
# Appendix D.
|
|
OPENID1_URL_LIMIT = 2047
|
|
|
|
# All OpenID protocol fields. Used to check namespace aliases.
|
|
OPENID_PROTOCOL_FIELDS = [
|
|
'ns', 'mode', 'error', 'return_to',
|
|
'contact', 'reference', 'signed',
|
|
'assoc_type', 'session_type',
|
|
'dh_modulus', 'dh_gen',
|
|
'dh_consumer_public', 'claimed_id',
|
|
'identity', 'realm', 'invalidate_handle',
|
|
'op_endpoint', 'response_nonce', 'sig',
|
|
'assoc_handle', 'trust_root', 'openid',
|
|
]
|
|
|
|
# Sentinel used for Message implementation to indicate that getArg
|
|
# should raise an exception instead of returning a default.
|
|
NO_DEFAULT = :no_default
|
|
|
|
# Raised if the generic OpenID namespace is accessed when there
|
|
# is no OpenID namespace set for this message.
|
|
class UndefinedOpenIDNamespace < Exception; end
|
|
|
|
# Raised when an alias or namespace URI has already been registered.
|
|
class NamespaceAliasRegistrationError < Exception; end
|
|
|
|
# Raised if openid.ns is not a recognized value.
|
|
# See Message class variable @@allowed_openid_namespaces
|
|
class InvalidOpenIDNamespace < Exception; end
|
|
|
|
class Message
|
|
attr_reader :namespaces
|
|
|
|
# Raised when key lookup fails
|
|
class KeyNotFound < IndexError ; end
|
|
|
|
# Namespace / alias registration map. See
|
|
# register_namespace_alias.
|
|
@@registered_aliases = {}
|
|
|
|
# Registers a (namespace URI, alias) mapping in a global namespace
|
|
# alias map. Raises NamespaceAliasRegistrationError if either the
|
|
# namespace URI or alias has already been registered with a
|
|
# different value. This function is required if you want to use a
|
|
# namespace with an OpenID 1 message.
|
|
def Message.register_namespace_alias(namespace_uri, alias_)
|
|
if @@registered_aliases[alias_] == namespace_uri
|
|
return
|
|
end
|
|
|
|
if @@registered_aliases.values.include?(namespace_uri)
|
|
raise NamespaceAliasRegistrationError,
|
|
'Namespace uri #{namespace_uri} already registered'
|
|
end
|
|
|
|
if @@registered_aliases.member?(alias_)
|
|
raise NamespaceAliasRegistrationError,
|
|
'Alias #{alias_} already registered'
|
|
end
|
|
|
|
@@registered_aliases[alias_] = namespace_uri
|
|
end
|
|
|
|
@@allowed_openid_namespaces = [OPENID1_NS, OPENID2_NS, OPENID11_NS]
|
|
|
|
# Raises InvalidNamespaceError if you try to instantiate a Message
|
|
# with a namespace not in the above allowed list
|
|
def initialize(openid_namespace=nil)
|
|
@args = {}
|
|
@namespaces = NamespaceMap.new
|
|
if openid_namespace
|
|
implicit = OPENID1_NAMESPACES.member? openid_namespace
|
|
self.set_openid_namespace(openid_namespace, implicit)
|
|
else
|
|
@openid_ns_uri = nil
|
|
end
|
|
end
|
|
|
|
# Construct a Message containing a set of POST arguments.
|
|
# Raises InvalidNamespaceError if you try to instantiate a Message
|
|
# with a namespace not in the above allowed list
|
|
def Message.from_post_args(args)
|
|
m = Message.new
|
|
openid_args = {}
|
|
args.each do |key,value|
|
|
if value.is_a?(Array)
|
|
raise ArgumentError, "Query dict must have one value for each key, " +
|
|
"not lists of values. Query is #{args.inspect}"
|
|
end
|
|
|
|
prefix, rest = key.split('.', 2)
|
|
|
|
if prefix != 'openid' or rest.nil?
|
|
m.set_arg(BARE_NS, key, value)
|
|
else
|
|
openid_args[rest] = value
|
|
end
|
|
end
|
|
|
|
m._from_openid_args(openid_args)
|
|
return m
|
|
end
|
|
|
|
# Construct a Message from a parsed KVForm message.
|
|
# Raises InvalidNamespaceError if you try to instantiate a Message
|
|
# with a namespace not in the above allowed list
|
|
def Message.from_openid_args(openid_args)
|
|
m = Message.new
|
|
m._from_openid_args(openid_args)
|
|
return m
|
|
end
|
|
|
|
# Raises InvalidNamespaceError if you try to instantiate a Message
|
|
# with a namespace not in the above allowed list
|
|
def _from_openid_args(openid_args)
|
|
ns_args = []
|
|
|
|
# resolve namespaces
|
|
openid_args.each { |rest, value|
|
|
ns_alias, ns_key = rest.split('.', 2)
|
|
if ns_key.nil?
|
|
ns_alias = NULL_NAMESPACE
|
|
ns_key = rest
|
|
end
|
|
|
|
if ns_alias == 'ns'
|
|
@namespaces.add_alias(value, ns_key)
|
|
elsif ns_alias == NULL_NAMESPACE and ns_key == 'ns'
|
|
set_openid_namespace(value, false)
|
|
else
|
|
ns_args << [ns_alias, ns_key, value]
|
|
end
|
|
}
|
|
|
|
# implicitly set an OpenID 1 namespace
|
|
unless get_openid_namespace
|
|
set_openid_namespace(OPENID1_NS, true)
|
|
end
|
|
|
|
# put the pairs into the appropriate namespaces
|
|
ns_args.each { |ns_alias, ns_key, value|
|
|
ns_uri = @namespaces.get_namespace_uri(ns_alias)
|
|
unless ns_uri
|
|
ns_uri = _get_default_namespace(ns_alias)
|
|
unless ns_uri
|
|
ns_uri = get_openid_namespace
|
|
ns_key = "#{ns_alias}.#{ns_key}"
|
|
else
|
|
@namespaces.add_alias(ns_uri, ns_alias, true)
|
|
end
|
|
end
|
|
self.set_arg(ns_uri, ns_key, value)
|
|
}
|
|
end
|
|
|
|
def _get_default_namespace(mystery_alias)
|
|
# only try to map an alias to a default if it's an
|
|
# OpenID 1.x namespace
|
|
if is_openid1
|
|
@@registered_aliases[mystery_alias]
|
|
end
|
|
end
|
|
|
|
def set_openid_namespace(openid_ns_uri, implicit)
|
|
if !@@allowed_openid_namespaces.include?(openid_ns_uri)
|
|
raise InvalidOpenIDNamespace, "Invalid null namespace: #{openid_ns_uri}"
|
|
end
|
|
@namespaces.add_alias(openid_ns_uri, NULL_NAMESPACE, implicit)
|
|
@openid_ns_uri = openid_ns_uri
|
|
end
|
|
|
|
def get_openid_namespace
|
|
return @openid_ns_uri
|
|
end
|
|
|
|
def is_openid1
|
|
return OPENID1_NAMESPACES.member?(@openid_ns_uri)
|
|
end
|
|
|
|
def is_openid2
|
|
return @openid_ns_uri == OPENID2_NS
|
|
end
|
|
|
|
# Create a message from a KVForm string
|
|
def Message.from_kvform(kvform_string)
|
|
return Message.from_openid_args(Util.kv_to_dict(kvform_string))
|
|
end
|
|
|
|
def copy
|
|
return Marshal.load(Marshal.dump(self))
|
|
end
|
|
|
|
# Return all arguments with "openid." in from of namespaced arguments.
|
|
def to_post_args
|
|
args = {}
|
|
|
|
# add namespace defs to the output
|
|
@namespaces.each { |ns_uri, ns_alias|
|
|
if @namespaces.implicit?(ns_uri)
|
|
next
|
|
end
|
|
if ns_alias == NULL_NAMESPACE
|
|
ns_key = 'openid.ns'
|
|
else
|
|
ns_key = 'openid.ns.' + ns_alias
|
|
end
|
|
args[ns_key] = ns_uri
|
|
}
|
|
|
|
@args.each { |k, value|
|
|
ns_uri, ns_key = k
|
|
key = get_key(ns_uri, ns_key)
|
|
args[key] = value
|
|
}
|
|
|
|
return args
|
|
end
|
|
|
|
# Return all namespaced arguments, failing if any non-namespaced arguments
|
|
# exist.
|
|
def to_args
|
|
post_args = self.to_post_args
|
|
kvargs = {}
|
|
post_args.each { |k,v|
|
|
if !k.starts_with?('openid.')
|
|
raise ArgumentError, "This message can only be encoded as a POST, because it contains arguments that are not prefixed with 'openid.'"
|
|
else
|
|
kvargs[k[7..-1]] = v
|
|
end
|
|
}
|
|
return kvargs
|
|
end
|
|
|
|
# Generate HTML form markup that contains the values in this
|
|
# message, to be HTTP POSTed as x-www-form-urlencoded UTF-8.
|
|
def to_form_markup(action_url, form_tag_attrs=nil, submit_text='Continue')
|
|
form_tag_attr_map = {}
|
|
|
|
if form_tag_attrs
|
|
form_tag_attrs.each { |name, attr|
|
|
form_tag_attr_map[name] = attr
|
|
}
|
|
end
|
|
|
|
form_tag_attr_map['action'] = action_url
|
|
form_tag_attr_map['method'] = 'post'
|
|
form_tag_attr_map['accept-charset'] = 'UTF-8'
|
|
form_tag_attr_map['enctype'] = 'application/x-www-form-urlencoded'
|
|
|
|
markup = "<form "
|
|
|
|
form_tag_attr_map.each { |k, v|
|
|
markup += " #{k}=\"#{v}\""
|
|
}
|
|
|
|
markup += ">\n"
|
|
|
|
to_post_args.each { |k,v|
|
|
markup += "<input type='hidden' name='#{k}' value='#{v}' />\n"
|
|
}
|
|
markup += "<input type='submit' value='#{submit_text}' />\n"
|
|
markup += "\n</form>"
|
|
return markup
|
|
end
|
|
|
|
# Generate a GET URL with the paramters in this message attacked as
|
|
# query parameters.
|
|
def to_url(base_url)
|
|
return Util.append_args(base_url, self.to_post_args)
|
|
end
|
|
|
|
# Generate a KVForm string that contains the parameters in this message.
|
|
# This will fail is the message contains arguments outside of the
|
|
# "openid." prefix.
|
|
def to_kvform
|
|
return Util.dict_to_kv(to_args)
|
|
end
|
|
|
|
# Generate an x-www-urlencoded string.
|
|
def to_url_encoded
|
|
args = to_post_args.map.sort
|
|
return Util.urlencode(args)
|
|
end
|
|
|
|
# Convert an input value into the internally used values of this obejct.
|
|
def _fix_ns(namespace)
|
|
if namespace == OPENID_NS
|
|
unless @openid_ns_uri
|
|
raise UndefinedOpenIDNamespace, 'OpenID namespace not set'
|
|
else
|
|
namespace = @openid_ns_uri
|
|
end
|
|
end
|
|
|
|
if namespace == BARE_NS
|
|
return namespace
|
|
end
|
|
|
|
if !namespace.is_a?(String)
|
|
raise ArgumentError, ("Namespace must be BARE_NS, OPENID_NS or "\
|
|
"a string. Got #{namespace.inspect}")
|
|
end
|
|
|
|
if namespace.index(':').nil?
|
|
msg = ("OpenID 2.0 namespace identifiers SHOULD be URIs. "\
|
|
"Got #{namespace.inspect}")
|
|
Util.log(msg)
|
|
|
|
if namespace == 'sreg'
|
|
msg = "Using #{SREG_URI} instead of \"sreg\" as namespace"
|
|
Util.log(msg)
|
|
return SREG_URI
|
|
end
|
|
end
|
|
|
|
return namespace
|
|
end
|
|
|
|
def has_key?(namespace, ns_key)
|
|
namespace = _fix_ns(namespace)
|
|
return @args.member?([namespace, ns_key])
|
|
end
|
|
|
|
# Get the key for a particular namespaced argument
|
|
def get_key(namespace, ns_key)
|
|
namespace = _fix_ns(namespace)
|
|
return ns_key if namespace == BARE_NS
|
|
|
|
ns_alias = @namespaces.get_alias(namespace)
|
|
|
|
# no alias is defined, so no key can exist
|
|
return nil if ns_alias.nil?
|
|
|
|
if ns_alias == NULL_NAMESPACE
|
|
tail = ns_key
|
|
else
|
|
tail = "#{ns_alias}.#{ns_key}"
|
|
end
|
|
|
|
return 'openid.' + tail
|
|
end
|
|
|
|
# Get a value for a namespaced key.
|
|
def get_arg(namespace, key, default=nil)
|
|
namespace = _fix_ns(namespace)
|
|
@args.fetch([namespace, key]) {
|
|
if default == NO_DEFAULT
|
|
raise KeyNotFound, "<#{namespace}>#{key} not in this message"
|
|
else
|
|
default
|
|
end
|
|
}
|
|
end
|
|
|
|
# Get the arguments that are defined for this namespace URI.
|
|
def get_args(namespace)
|
|
namespace = _fix_ns(namespace)
|
|
args = {}
|
|
@args.each { |k,v|
|
|
pair_ns, ns_key = k
|
|
args[ns_key] = v if pair_ns == namespace
|
|
}
|
|
return args
|
|
end
|
|
|
|
# Set multiple key/value pairs in one call.
|
|
def update_args(namespace, updates)
|
|
namespace = _fix_ns(namespace)
|
|
updates.each {|k,v| set_arg(namespace, k, v)}
|
|
end
|
|
|
|
# Set a single argument in this namespace
|
|
def set_arg(namespace, key, value)
|
|
namespace = _fix_ns(namespace)
|
|
@args[[namespace, key].freeze] = value
|
|
if namespace != BARE_NS
|
|
@namespaces.add(namespace)
|
|
end
|
|
end
|
|
|
|
# Remove a single argument from this namespace.
|
|
def del_arg(namespace, key)
|
|
namespace = _fix_ns(namespace)
|
|
_key = [namespace, key]
|
|
@args.delete(_key)
|
|
end
|
|
|
|
def ==(other)
|
|
other.is_a?(self.class) && @args == other.instance_eval { @args }
|
|
end
|
|
|
|
def get_aliased_arg(aliased_key, default=nil)
|
|
if aliased_key == 'ns'
|
|
return get_openid_namespace()
|
|
end
|
|
|
|
ns_alias, key = aliased_key.split('.', 2)
|
|
if ns_alias == 'ns'
|
|
uri = @namespaces.get_namespace_uri(key)
|
|
if uri.nil? and default == NO_DEFAULT
|
|
raise KeyNotFound, "Namespace #{key} not defined when looking "\
|
|
"for #{aliased_key}"
|
|
else
|
|
return (uri.nil? ? default : uri)
|
|
end
|
|
end
|
|
|
|
if key.nil?
|
|
key = aliased_key
|
|
ns = nil
|
|
else
|
|
ns = @namespaces.get_namespace_uri(ns_alias)
|
|
end
|
|
|
|
if ns.nil?
|
|
key = aliased_key
|
|
ns = get_openid_namespace
|
|
end
|
|
|
|
return get_arg(ns, key, default)
|
|
end
|
|
end
|
|
|
|
|
|
# Maintains a bidirectional map between namespace URIs and aliases.
|
|
class NamespaceMap
|
|
|
|
def initialize
|
|
@alias_to_namespace = {}
|
|
@namespace_to_alias = {}
|
|
@implicit_namespaces = []
|
|
end
|
|
|
|
def get_alias(namespace_uri)
|
|
@namespace_to_alias[namespace_uri]
|
|
end
|
|
|
|
def get_namespace_uri(namespace_alias)
|
|
@alias_to_namespace[namespace_alias]
|
|
end
|
|
|
|
# Add an alias from this namespace URI to the alias.
|
|
def add_alias(namespace_uri, desired_alias, implicit=false)
|
|
# Check that desired_alias is not an openid protocol field as
|
|
# per the spec.
|
|
Util.assert(!OPENID_PROTOCOL_FIELDS.include?(desired_alias),
|
|
"#{desired_alias} is not an allowed namespace alias")
|
|
|
|
# check that there is not a namespace already defined for the
|
|
# desired alias
|
|
current_namespace_uri = @alias_to_namespace.fetch(desired_alias, nil)
|
|
if current_namespace_uri and current_namespace_uri != namespace_uri
|
|
raise IndexError, "Cannot map #{namespace_uri} to alias #{desired_alias}. #{current_namespace_uri} is already mapped to alias #{desired_alias}"
|
|
end
|
|
|
|
# Check that desired_alias does not contain a period as per the
|
|
# spec.
|
|
if desired_alias.is_a?(String)
|
|
Util.assert(desired_alias.index('.').nil?,
|
|
"#{desired_alias} must not contain a dot")
|
|
end
|
|
|
|
# check that there is not already a (different) alias for this
|
|
# namespace URI.
|
|
_alias = @namespace_to_alias[namespace_uri]
|
|
if _alias and _alias != desired_alias
|
|
raise IndexError, "Cannot map #{namespace_uri} to alias #{desired_alias}. It is already mapped to alias #{_alias}"
|
|
end
|
|
|
|
@alias_to_namespace[desired_alias] = namespace_uri
|
|
@namespace_to_alias[namespace_uri] = desired_alias
|
|
@implicit_namespaces << namespace_uri if implicit
|
|
return desired_alias
|
|
end
|
|
|
|
# Add this namespace URI to the mapping, without caring what alias
|
|
# it ends up with.
|
|
def add(namespace_uri)
|
|
# see if this namepace is already mapped to an alias
|
|
_alias = @namespace_to_alias[namespace_uri]
|
|
return _alias if _alias
|
|
|
|
# Fall back to generating a numberical alias
|
|
i = 0
|
|
while true
|
|
_alias = 'ext' + i.to_s
|
|
begin
|
|
add_alias(namespace_uri, _alias)
|
|
rescue IndexError
|
|
i += 1
|
|
else
|
|
return _alias
|
|
end
|
|
end
|
|
|
|
raise StandardError, 'Unreachable'
|
|
end
|
|
|
|
def member?(namespace_uri)
|
|
@namespace_to_alias.has_key?(namespace_uri)
|
|
end
|
|
|
|
def each
|
|
@namespace_to_alias.each {|k,v| yield k,v}
|
|
end
|
|
|
|
def namespace_uris
|
|
# Return an iterator over the namespace URIs
|
|
return @namespace_to_alias.keys()
|
|
end
|
|
|
|
def implicit?(namespace_uri)
|
|
return @implicit_namespaces.member?(namespace_uri)
|
|
end
|
|
|
|
def aliases
|
|
# Return an iterator over the aliases
|
|
return @alias_to_namespace.keys()
|
|
end
|
|
end
|
|
end
|