git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@2437 e93f8b46-1217-0410-a6f0-8f06a7374b81
1545 lines
50 KiB
Ruby
1545 lines
50 KiB
Ruby
|
|
require 'openid/cryptutil'
|
|
require 'openid/util'
|
|
require 'openid/dh'
|
|
require 'openid/store/nonce'
|
|
require 'openid/trustroot'
|
|
require 'openid/association'
|
|
require 'openid/message'
|
|
|
|
require 'time'
|
|
|
|
module OpenID
|
|
|
|
module Server
|
|
|
|
HTTP_OK = 200
|
|
HTTP_REDIRECT = 302
|
|
HTTP_ERROR = 400
|
|
|
|
BROWSER_REQUEST_MODES = ['checkid_setup', 'checkid_immediate']
|
|
|
|
ENCODE_KVFORM = ['kvform'].freeze
|
|
ENCODE_URL = ['URL/redirect'].freeze
|
|
ENCODE_HTML_FORM = ['HTML form'].freeze
|
|
|
|
UNUSED = nil
|
|
|
|
class OpenIDRequest
|
|
attr_accessor :message, :mode
|
|
|
|
# I represent an incoming OpenID request.
|
|
#
|
|
# Attributes:
|
|
# mode:: The "openid.mode" of this request
|
|
def initialize
|
|
@mode = nil
|
|
@message = nil
|
|
end
|
|
|
|
def namespace
|
|
if @message.nil?
|
|
raise RuntimeError, "Request has no message"
|
|
else
|
|
return @message.get_openid_namespace
|
|
end
|
|
end
|
|
end
|
|
|
|
# A request to verify the validity of a previous response.
|
|
#
|
|
# See OpenID Specs, Verifying Directly with the OpenID Provider
|
|
# <http://openid.net/specs/openid-authentication-2_0-12.html#verifying_signatures>
|
|
class CheckAuthRequest < OpenIDRequest
|
|
|
|
# The association handle the response was signed with.
|
|
attr_accessor :assoc_handle
|
|
|
|
# The message with the signature which wants checking.
|
|
attr_accessor :signed
|
|
|
|
# An association handle the client is asking about the validity
|
|
# of. May be nil.
|
|
attr_accessor :invalidate_handle
|
|
|
|
attr_accessor :sig
|
|
|
|
# Construct me.
|
|
#
|
|
# These parameters are assigned directly as class attributes.
|
|
#
|
|
# Parameters:
|
|
# assoc_handle:: the association handle for this request
|
|
# signed:: The signed message
|
|
# invalidate_handle:: An association handle that the relying
|
|
# party is checking to see if it is invalid
|
|
def initialize(assoc_handle, signed, invalidate_handle=nil)
|
|
super()
|
|
|
|
@mode = "check_authentication"
|
|
@required_fields = ["identity", "return_to", "response_nonce"].freeze
|
|
|
|
@sig = nil
|
|
@assoc_handle = assoc_handle
|
|
@signed = signed
|
|
@invalidate_handle = invalidate_handle
|
|
end
|
|
|
|
# Construct me from an OpenID::Message.
|
|
def self.from_message(message, op_endpoint=UNUSED)
|
|
assoc_handle = message.get_arg(OPENID_NS, 'assoc_handle')
|
|
invalidate_handle = message.get_arg(OPENID_NS, 'invalidate_handle')
|
|
|
|
signed = message.copy()
|
|
# openid.mode is currently check_authentication because
|
|
# that's the mode of this request. But the signature
|
|
# was made on something with a different openid.mode.
|
|
# http://article.gmane.org/gmane.comp.web.openid.general/537
|
|
if signed.has_key?(OPENID_NS, "mode")
|
|
signed.set_arg(OPENID_NS, "mode", "id_res")
|
|
end
|
|
|
|
obj = self.new(assoc_handle, signed, invalidate_handle)
|
|
obj.message = message
|
|
obj.sig = message.get_arg(OPENID_NS, 'sig')
|
|
|
|
if !obj.assoc_handle or
|
|
!obj.sig
|
|
msg = sprintf("%s request missing required parameter from message %s",
|
|
obj.mode, message)
|
|
raise ProtocolError.new(message, msg)
|
|
end
|
|
|
|
return obj
|
|
end
|
|
|
|
# Respond to this request.
|
|
#
|
|
# Given a Signatory, I can check the validity of the signature
|
|
# and the invalidate_handle. I return a response with an
|
|
# is_valid (and, if appropriate invalidate_handle) field.
|
|
def answer(signatory)
|
|
is_valid = signatory.verify(@assoc_handle, @signed)
|
|
# Now invalidate that assoc_handle so it this checkAuth
|
|
# message cannot be replayed.
|
|
signatory.invalidate(@assoc_handle, dumb=true)
|
|
response = OpenIDResponse.new(self)
|
|
valid_str = is_valid ? "true" : "false"
|
|
response.fields.set_arg(OPENID_NS, 'is_valid', valid_str)
|
|
|
|
if @invalidate_handle
|
|
assoc = signatory.get_association(@invalidate_handle, false)
|
|
if !assoc
|
|
response.fields.set_arg(
|
|
OPENID_NS, 'invalidate_handle', @invalidate_handle)
|
|
end
|
|
end
|
|
|
|
return response
|
|
end
|
|
|
|
def to_s
|
|
ih = nil
|
|
|
|
if @invalidate_handle
|
|
ih = sprintf(" invalidate? %s", @invalidate_handle)
|
|
else
|
|
ih = ""
|
|
end
|
|
|
|
s = sprintf("<%s handle: %s sig: %s: signed: %s%s>",
|
|
self.class, @assoc_handle,
|
|
@sig, @signed, ih)
|
|
return s
|
|
end
|
|
end
|
|
|
|
class BaseServerSession
|
|
attr_reader :session_type
|
|
|
|
def initialize(session_type, allowed_assoc_types)
|
|
@session_type = session_type
|
|
@allowed_assoc_types = allowed_assoc_types.dup.freeze
|
|
end
|
|
|
|
def allowed_assoc_type?(typ)
|
|
@allowed_assoc_types.member?(typ)
|
|
end
|
|
end
|
|
|
|
# An object that knows how to handle association requests with
|
|
# no session type.
|
|
#
|
|
# See OpenID Specs, Section 8: Establishing Associations
|
|
# <http://openid.net/specs/openid-authentication-2_0-12.html#associations>
|
|
class PlainTextServerSession < BaseServerSession
|
|
# The session_type for this association session. There is no
|
|
# type defined for plain-text in the OpenID specification, so we
|
|
# use 'no-encryption'.
|
|
attr_reader :session_type
|
|
|
|
def initialize
|
|
super('no-encryption', ['HMAC-SHA1', 'HMAC-SHA256'])
|
|
end
|
|
|
|
def self.from_message(unused_request)
|
|
return self.new
|
|
end
|
|
|
|
def answer(secret)
|
|
return {'mac_key' => Util.to_base64(secret)}
|
|
end
|
|
end
|
|
|
|
# An object that knows how to handle association requests with the
|
|
# Diffie-Hellman session type.
|
|
#
|
|
# See OpenID Specs, Section 8: Establishing Associations
|
|
# <http://openid.net/specs/openid-authentication-2_0-12.html#associations>
|
|
class DiffieHellmanSHA1ServerSession < BaseServerSession
|
|
|
|
# The Diffie-Hellman algorithm values for this request
|
|
attr_accessor :dh
|
|
|
|
# The public key sent by the consumer in the associate request
|
|
attr_accessor :consumer_pubkey
|
|
|
|
# The session_type for this association session.
|
|
attr_reader :session_type
|
|
|
|
def initialize(dh, consumer_pubkey)
|
|
super('DH-SHA1', ['HMAC-SHA1'])
|
|
|
|
@hash_func = CryptUtil.method('sha1')
|
|
@dh = dh
|
|
@consumer_pubkey = consumer_pubkey
|
|
end
|
|
|
|
# Construct me from OpenID Message
|
|
#
|
|
# Raises ProtocolError when parameters required to establish the
|
|
# session are missing.
|
|
def self.from_message(message)
|
|
dh_modulus = message.get_arg(OPENID_NS, 'dh_modulus')
|
|
dh_gen = message.get_arg(OPENID_NS, 'dh_gen')
|
|
if ((!dh_modulus and dh_gen) or
|
|
(!dh_gen and dh_modulus))
|
|
|
|
if !dh_modulus
|
|
missing = 'modulus'
|
|
else
|
|
missing = 'generator'
|
|
end
|
|
|
|
raise ProtocolError.new(message,
|
|
sprintf('If non-default modulus or generator is ' +
|
|
'supplied, both must be supplied. Missing %s',
|
|
missing))
|
|
end
|
|
|
|
if dh_modulus or dh_gen
|
|
dh_modulus = CryptUtil.base64_to_num(dh_modulus)
|
|
dh_gen = CryptUtil.base64_to_num(dh_gen)
|
|
dh = DiffieHellman.new(dh_modulus, dh_gen)
|
|
else
|
|
dh = DiffieHellman.from_defaults()
|
|
end
|
|
|
|
consumer_pubkey = message.get_arg(OPENID_NS, 'dh_consumer_public')
|
|
if !consumer_pubkey
|
|
raise ProtocolError.new(message,
|
|
sprintf("Public key for DH-SHA1 session " +
|
|
"not found in message %s", message))
|
|
end
|
|
|
|
consumer_pubkey = CryptUtil.base64_to_num(consumer_pubkey)
|
|
|
|
return self.new(dh, consumer_pubkey)
|
|
end
|
|
|
|
def answer(secret)
|
|
mac_key = @dh.xor_secret(@hash_func,
|
|
@consumer_pubkey,
|
|
secret)
|
|
return {
|
|
'dh_server_public' => CryptUtil.num_to_base64(@dh.public),
|
|
'enc_mac_key' => Util.to_base64(mac_key),
|
|
}
|
|
end
|
|
end
|
|
|
|
class DiffieHellmanSHA256ServerSession < DiffieHellmanSHA1ServerSession
|
|
def initialize(*args)
|
|
super(*args)
|
|
@session_type = 'DH-SHA256'
|
|
@hash_func = CryptUtil.method('sha256')
|
|
@allowed_assoc_types = ['HMAC-SHA256'].freeze
|
|
end
|
|
end
|
|
|
|
# A request to establish an association.
|
|
#
|
|
# See OpenID Specs, Section 8: Establishing Associations
|
|
# <http://openid.net/specs/openid-authentication-2_0-12.html#associations>
|
|
class AssociateRequest < OpenIDRequest
|
|
# An object that knows how to handle association requests of a
|
|
# certain type.
|
|
attr_accessor :session
|
|
|
|
# The type of association. Supported values include HMAC-SHA256
|
|
# and HMAC-SHA1
|
|
attr_accessor :assoc_type
|
|
|
|
@@session_classes = {
|
|
'no-encryption' => PlainTextServerSession,
|
|
'DH-SHA1' => DiffieHellmanSHA1ServerSession,
|
|
'DH-SHA256' => DiffieHellmanSHA256ServerSession,
|
|
}
|
|
|
|
# Construct me.
|
|
#
|
|
# The session is assigned directly as a class attribute. See my
|
|
# class documentation for its description.
|
|
def initialize(session, assoc_type)
|
|
super()
|
|
@session = session
|
|
@assoc_type = assoc_type
|
|
|
|
@mode = "associate"
|
|
end
|
|
|
|
# Construct me from an OpenID Message.
|
|
def self.from_message(message, op_endpoint=UNUSED)
|
|
if message.is_openid1()
|
|
session_type = message.get_arg(OPENID_NS, 'session_type')
|
|
if session_type == 'no-encryption'
|
|
Util.log('Received OpenID 1 request with a no-encryption ' +
|
|
'association session type. Continuing anyway.')
|
|
elsif !session_type
|
|
session_type = 'no-encryption'
|
|
end
|
|
else
|
|
session_type = message.get_arg(OPENID2_NS, 'session_type')
|
|
if !session_type
|
|
raise ProtocolError.new(message,
|
|
text="session_type missing from request")
|
|
end
|
|
end
|
|
|
|
session_class = @@session_classes[session_type]
|
|
|
|
if !session_class
|
|
raise ProtocolError.new(message,
|
|
sprintf("Unknown session type %s", session_type))
|
|
end
|
|
|
|
begin
|
|
session = session_class.from_message(message)
|
|
rescue ArgumentError => why
|
|
# XXX
|
|
raise ProtocolError.new(message,
|
|
sprintf('Error parsing %s session: %s',
|
|
session_type, why))
|
|
end
|
|
|
|
assoc_type = message.get_arg(OPENID_NS, 'assoc_type', 'HMAC-SHA1')
|
|
if !session.allowed_assoc_type?(assoc_type)
|
|
msg = sprintf('Session type %s does not support association type %s',
|
|
session_type, assoc_type)
|
|
raise ProtocolError.new(message, msg)
|
|
end
|
|
|
|
obj = self.new(session, assoc_type)
|
|
obj.message = message
|
|
return obj
|
|
end
|
|
|
|
# Respond to this request with an association.
|
|
#
|
|
# assoc:: The association to send back.
|
|
#
|
|
# Returns a response with the association information, encrypted
|
|
# to the consumer's public key if appropriate.
|
|
def answer(assoc)
|
|
response = OpenIDResponse.new(self)
|
|
response.fields.update_args(OPENID_NS, {
|
|
'expires_in' => sprintf('%d', assoc.expires_in()),
|
|
'assoc_type' => @assoc_type,
|
|
'assoc_handle' => assoc.handle,
|
|
})
|
|
response.fields.update_args(OPENID_NS,
|
|
@session.answer(assoc.secret))
|
|
unless (@session.session_type == 'no-encryption' and
|
|
@message.is_openid1)
|
|
response.fields.set_arg(
|
|
OPENID_NS, 'session_type', @session.session_type)
|
|
end
|
|
|
|
return response
|
|
end
|
|
|
|
# Respond to this request indicating that the association type
|
|
# or association session type is not supported.
|
|
def answer_unsupported(message, preferred_association_type=nil,
|
|
preferred_session_type=nil)
|
|
if @message.is_openid1()
|
|
raise ProtocolError.new(@message)
|
|
end
|
|
|
|
response = OpenIDResponse.new(self)
|
|
response.fields.set_arg(OPENID_NS, 'error_code', 'unsupported-type')
|
|
response.fields.set_arg(OPENID_NS, 'error', message)
|
|
|
|
if preferred_association_type
|
|
response.fields.set_arg(
|
|
OPENID_NS, 'assoc_type', preferred_association_type)
|
|
end
|
|
|
|
if preferred_session_type
|
|
response.fields.set_arg(
|
|
OPENID_NS, 'session_type', preferred_session_type)
|
|
end
|
|
|
|
return response
|
|
end
|
|
end
|
|
|
|
# A request to confirm the identity of a user.
|
|
#
|
|
# This class handles requests for openid modes
|
|
# +checkid_immediate+ and +checkid_setup+ .
|
|
class CheckIDRequest < OpenIDRequest
|
|
|
|
# Provided in smart mode requests, a handle for a previously
|
|
# established association. nil for dumb mode requests.
|
|
attr_accessor :assoc_handle
|
|
|
|
# Is this an immediate-mode request?
|
|
attr_accessor :immediate
|
|
|
|
# The URL to send the user agent back to to reply to this
|
|
# request.
|
|
attr_accessor :return_to
|
|
|
|
# The OP-local identifier being checked.
|
|
attr_accessor :identity
|
|
|
|
# The claimed identifier. Not present in OpenID 1.x
|
|
# messages.
|
|
attr_accessor :claimed_id
|
|
|
|
# This URL identifies the party making the request, and the user
|
|
# will use that to make her decision about what answer she
|
|
# trusts them to have. Referred to as "realm" in OpenID 2.0.
|
|
attr_accessor :trust_root
|
|
|
|
# mode:: +checkid_immediate+ or +checkid_setup+
|
|
attr_accessor :mode
|
|
|
|
attr_accessor :op_endpoint
|
|
|
|
# These parameters are assigned directly as attributes,
|
|
# see the #CheckIDRequest class documentation for their
|
|
# descriptions.
|
|
#
|
|
# Raises #MalformedReturnURL when the +return_to+ URL is not
|
|
# a URL.
|
|
def initialize(identity, return_to, op_endpoint, trust_root=nil,
|
|
immediate=false, assoc_handle=nil, claimed_id=nil)
|
|
@assoc_handle = assoc_handle
|
|
@identity = identity
|
|
@claimed_id = (claimed_id or identity)
|
|
@return_to = return_to
|
|
@trust_root = (trust_root or return_to)
|
|
@op_endpoint = op_endpoint
|
|
@message = nil
|
|
|
|
if immediate
|
|
@immediate = true
|
|
@mode = "checkid_immediate"
|
|
else
|
|
@immediate = false
|
|
@mode = "checkid_setup"
|
|
end
|
|
|
|
if @return_to and
|
|
!TrustRoot::TrustRoot.parse(@return_to)
|
|
raise MalformedReturnURL.new(nil, @return_to)
|
|
end
|
|
|
|
if !trust_root_valid()
|
|
raise UntrustedReturnURL.new(nil, @return_to, @trust_root)
|
|
end
|
|
end
|
|
|
|
# Construct me from an OpenID message.
|
|
#
|
|
# message:: An OpenID checkid_* request Message
|
|
#
|
|
# op_endpoint:: The endpoint URL of the server that this
|
|
# message was sent to.
|
|
#
|
|
# Raises:
|
|
# ProtocolError:: When not all required parameters are present
|
|
# in the message.
|
|
#
|
|
# MalformedReturnURL:: When the +return_to+ URL is not a URL.
|
|
#
|
|
# UntrustedReturnURL:: When the +return_to+ URL is
|
|
# outside the +trust_root+.
|
|
def self.from_message(message, op_endpoint)
|
|
obj = self.allocate
|
|
obj.message = message
|
|
obj.op_endpoint = op_endpoint
|
|
mode = message.get_arg(OPENID_NS, 'mode')
|
|
if mode == "checkid_immediate"
|
|
obj.immediate = true
|
|
obj.mode = "checkid_immediate"
|
|
else
|
|
obj.immediate = false
|
|
obj.mode = "checkid_setup"
|
|
end
|
|
|
|
obj.return_to = message.get_arg(OPENID_NS, 'return_to')
|
|
if message.is_openid1 and !obj.return_to
|
|
msg = sprintf("Missing required field 'return_to' from %s",
|
|
message)
|
|
raise ProtocolError.new(message, msg)
|
|
end
|
|
|
|
obj.identity = message.get_arg(OPENID_NS, 'identity')
|
|
obj.claimed_id = message.get_arg(OPENID_NS, 'claimed_id')
|
|
if message.is_openid1()
|
|
if !obj.identity
|
|
s = "OpenID 1 message did not contain openid.identity"
|
|
raise ProtocolError.new(message, s)
|
|
end
|
|
else
|
|
if obj.identity and not obj.claimed_id
|
|
s = ("OpenID 2.0 message contained openid.identity but not " +
|
|
"claimed_id")
|
|
raise ProtocolError.new(message, s)
|
|
elsif obj.claimed_id and not obj.identity
|
|
s = ("OpenID 2.0 message contained openid.claimed_id but not " +
|
|
"identity")
|
|
raise ProtocolError.new(message, s)
|
|
end
|
|
end
|
|
|
|
# There's a case for making self.trust_root be a TrustRoot
|
|
# here. But if TrustRoot isn't currently part of the "public"
|
|
# API, I'm not sure it's worth doing.
|
|
if message.is_openid1
|
|
trust_root_param = 'trust_root'
|
|
else
|
|
trust_root_param = 'realm'
|
|
end
|
|
trust_root = message.get_arg(OPENID_NS, trust_root_param)
|
|
trust_root = obj.return_to if (trust_root.nil? || trust_root.empty?)
|
|
obj.trust_root = trust_root
|
|
|
|
if !message.is_openid1 and !obj.return_to and !obj.trust_root
|
|
raise ProtocolError.new(message, "openid.realm required when " +
|
|
"openid.return_to absent")
|
|
end
|
|
|
|
obj.assoc_handle = message.get_arg(OPENID_NS, 'assoc_handle')
|
|
|
|
# Using TrustRoot.parse here is a bit misleading, as we're not
|
|
# parsing return_to as a trust root at all. However, valid
|
|
# URLs are valid trust roots, so we can use this to get an
|
|
# idea if it is a valid URL. Not all trust roots are valid
|
|
# return_to URLs, however (particularly ones with wildcards),
|
|
# so this is still a little sketchy.
|
|
if obj.return_to and \
|
|
!TrustRoot::TrustRoot.parse(obj.return_to)
|
|
raise MalformedReturnURL.new(message, obj.return_to)
|
|
end
|
|
|
|
# I first thought that checking to see if the return_to is
|
|
# within the trust_root is premature here, a
|
|
# logic-not-decoding thing. But it was argued that this is
|
|
# really part of data validation. A request with an invalid
|
|
# trust_root/return_to is broken regardless of application,
|
|
# right?
|
|
if !obj.trust_root_valid()
|
|
raise UntrustedReturnURL.new(message, obj.return_to, obj.trust_root)
|
|
end
|
|
|
|
return obj
|
|
end
|
|
|
|
# Is the identifier to be selected by the IDP?
|
|
def id_select
|
|
# So IDPs don't have to import the constant
|
|
return @identity == IDENTIFIER_SELECT
|
|
end
|
|
|
|
# Is my return_to under my trust_root?
|
|
def trust_root_valid
|
|
if !@trust_root
|
|
return true
|
|
end
|
|
|
|
tr = TrustRoot::TrustRoot.parse(@trust_root)
|
|
if !tr
|
|
raise MalformedTrustRoot.new(@message, @trust_root)
|
|
end
|
|
|
|
if @return_to
|
|
return tr.validate_url(@return_to)
|
|
else
|
|
return true
|
|
end
|
|
end
|
|
|
|
# Does the relying party publish the return_to URL for this
|
|
# response under the realm? It is up to the provider to set a
|
|
# policy for what kinds of realms should be allowed. This
|
|
# return_to URL verification reduces vulnerability to
|
|
# data-theft attacks based on open proxies,
|
|
# corss-site-scripting, or open redirectors.
|
|
#
|
|
# This check should only be performed after making sure that
|
|
# the return_to URL matches the realm.
|
|
#
|
|
# Raises DiscoveryFailure if the realm
|
|
# URL does not support Yadis discovery (and so does not
|
|
# support the verification process).
|
|
#
|
|
# Returns true if the realm publishes a document with the
|
|
# return_to URL listed
|
|
def return_to_verified
|
|
return TrustRoot.verify_return_to(@trust_root, @return_to)
|
|
end
|
|
|
|
# Respond to this request.
|
|
#
|
|
# allow:: Allow this user to claim this identity, and allow the
|
|
# consumer to have this information?
|
|
#
|
|
# server_url:: DEPRECATED. Passing op_endpoint to the
|
|
# #Server constructor makes this optional.
|
|
#
|
|
# When an OpenID 1.x immediate mode request does
|
|
# not succeed, it gets back a URL where the request
|
|
# may be carried out in a not-so-immediate fashion.
|
|
# Pass my URL in here (the fully qualified address
|
|
# of this server's endpoint, i.e.
|
|
# <tt>http://example.com/server</tt>), and I will
|
|
# use it as a base for the URL for a new request.
|
|
#
|
|
# Optional for requests where
|
|
# #CheckIDRequest.immediate is false or +allow+ is
|
|
# true.
|
|
#
|
|
# identity:: The OP-local identifier to answer with. Only for use
|
|
# when the relying party requested identifier selection.
|
|
#
|
|
# claimed_id:: The claimed identifier to answer with,
|
|
# for use with identifier selection in the case where the
|
|
# claimed identifier and the OP-local identifier differ,
|
|
# i.e. when the claimed_id uses delegation.
|
|
#
|
|
# If +identity+ is provided but this is not,
|
|
# +claimed_id+ will default to the value of +identity+.
|
|
# When answering requests that did not ask for identifier
|
|
# selection, the response +claimed_id+ will default to
|
|
# that of the request.
|
|
#
|
|
# This parameter is new in OpenID 2.0.
|
|
#
|
|
# Returns an OpenIDResponse object containing a OpenID id_res message.
|
|
#
|
|
# Raises NoReturnToError if the return_to is missing.
|
|
#
|
|
# Version 2.0 deprecates +server_url+ and adds +claimed_id+.
|
|
def answer(allow, server_url=nil, identity=nil, claimed_id=nil)
|
|
if !@return_to
|
|
raise NoReturnToError
|
|
end
|
|
|
|
if !server_url
|
|
if @message.is_openid2 and !@op_endpoint
|
|
# In other words, that warning I raised in
|
|
# Server.__init__? You should pay attention to it now.
|
|
raise RuntimeError, ("#{self} should be constructed with "\
|
|
"op_endpoint to respond to OpenID 2.0 "\
|
|
"messages.")
|
|
end
|
|
|
|
server_url = @op_endpoint
|
|
end
|
|
|
|
if allow
|
|
mode = 'id_res'
|
|
elsif @message.is_openid1
|
|
if @immediate
|
|
mode = 'id_res'
|
|
else
|
|
mode = 'cancel'
|
|
end
|
|
else
|
|
if @immediate
|
|
mode = 'setup_needed'
|
|
else
|
|
mode = 'cancel'
|
|
end
|
|
end
|
|
|
|
response = OpenIDResponse.new(self)
|
|
|
|
if claimed_id and @message.is_openid1
|
|
raise VersionError, ("claimed_id is new in OpenID 2.0 and not "\
|
|
"available for #{@message.get_openid_namespace}")
|
|
end
|
|
|
|
if identity and !claimed_id
|
|
claimed_id = identity
|
|
end
|
|
|
|
if allow
|
|
if @identity == IDENTIFIER_SELECT
|
|
if !identity
|
|
raise ArgumentError, ("This request uses IdP-driven "\
|
|
"identifier selection.You must supply "\
|
|
"an identifier in the response.")
|
|
end
|
|
|
|
response_identity = identity
|
|
response_claimed_id = claimed_id
|
|
|
|
elsif @identity
|
|
if identity and (@identity != identity)
|
|
raise ArgumentError, ("Request was for identity #{@identity}, "\
|
|
"cannot reply with identity #{identity}")
|
|
end
|
|
|
|
response_identity = @identity
|
|
response_claimed_id = @claimed_id
|
|
else
|
|
if identity
|
|
raise ArgumentError, ("This request specified no identity "\
|
|
"and you supplied #{identity}")
|
|
end
|
|
response_identity = nil
|
|
end
|
|
|
|
if @message.is_openid1 and !response_identity
|
|
raise ArgumentError, ("Request was an OpenID 1 request, so "\
|
|
"response must include an identifier.")
|
|
end
|
|
|
|
response.fields.update_args(OPENID_NS, {
|
|
'mode' => mode,
|
|
'op_endpoint' => server_url,
|
|
'return_to' => @return_to,
|
|
'response_nonce' => Nonce.mk_nonce(),
|
|
})
|
|
|
|
if response_identity
|
|
response.fields.set_arg(OPENID_NS, 'identity', response_identity)
|
|
if @message.is_openid2
|
|
response.fields.set_arg(OPENID_NS,
|
|
'claimed_id', response_claimed_id)
|
|
end
|
|
end
|
|
else
|
|
response.fields.set_arg(OPENID_NS, 'mode', mode)
|
|
if @immediate
|
|
if @message.is_openid1 and !server_url
|
|
raise ArgumentError, ("setup_url is required for allow=false "\
|
|
"in OpenID 1.x immediate mode.")
|
|
end
|
|
|
|
# Make a new request just like me, but with
|
|
# immediate=false.
|
|
setup_request = self.class.new(@identity, @return_to,
|
|
@op_endpoint, @trust_root, false,
|
|
@assoc_handle, @claimed_id)
|
|
setup_request.message = Message.new(@message.get_openid_namespace)
|
|
setup_url = setup_request.encode_to_url(server_url)
|
|
response.fields.set_arg(OPENID_NS, 'user_setup_url', setup_url)
|
|
end
|
|
end
|
|
|
|
return response
|
|
end
|
|
|
|
def encode_to_url(server_url)
|
|
# Encode this request as a URL to GET.
|
|
#
|
|
# server_url:: The URL of the OpenID server to make this
|
|
# request of.
|
|
if !@return_to
|
|
raise NoReturnToError
|
|
end
|
|
|
|
# Imported from the alternate reality where these classes are
|
|
# used in both the client and server code, so Requests are
|
|
# Encodable too. That's right, code imported from alternate
|
|
# realities all for the love of you, id_res/user_setup_url.
|
|
q = {'mode' => @mode,
|
|
'identity' => @identity,
|
|
'claimed_id' => @claimed_id,
|
|
'return_to' => @return_to}
|
|
|
|
if @trust_root
|
|
if @message.is_openid1
|
|
q['trust_root'] = @trust_root
|
|
else
|
|
q['realm'] = @trust_root
|
|
end
|
|
end
|
|
|
|
if @assoc_handle
|
|
q['assoc_handle'] = @assoc_handle
|
|
end
|
|
|
|
response = Message.new(@message.get_openid_namespace)
|
|
response.update_args(@message.get_openid_namespace, q)
|
|
return response.to_url(server_url)
|
|
end
|
|
|
|
def cancel_url
|
|
# Get the URL to cancel this request.
|
|
#
|
|
# Useful for creating a "Cancel" button on a web form so that
|
|
# operation can be carried out directly without another trip
|
|
# through the server.
|
|
#
|
|
# (Except you may want to make another trip through the
|
|
# server so that it knows that the user did make a decision.)
|
|
#
|
|
# Returns a URL as a string.
|
|
if !@return_to
|
|
raise NoReturnToError
|
|
end
|
|
|
|
if @immediate
|
|
raise ArgumentError.new("Cancel is not an appropriate response to " +
|
|
"immediate mode requests.")
|
|
end
|
|
|
|
response = Message.new(@message.get_openid_namespace)
|
|
response.set_arg(OPENID_NS, 'mode', 'cancel')
|
|
return response.to_url(@return_to)
|
|
end
|
|
|
|
def to_s
|
|
return sprintf('<%s id:%s im:%s tr:%s ah:%s>', self.class,
|
|
@identity,
|
|
@immediate,
|
|
@trust_root,
|
|
@assoc_handle)
|
|
end
|
|
end
|
|
|
|
# I am a response to an OpenID request.
|
|
#
|
|
# Attributes:
|
|
# signed:: A list of the names of the fields which should be signed.
|
|
#
|
|
# Implementer's note: In a more symmetric client/server
|
|
# implementation, there would be more types of #OpenIDResponse
|
|
# object and they would have validated attributes according to
|
|
# the type of response. But as it is, Response objects in a
|
|
# server are basically write-only, their only job is to go out
|
|
# over the wire, so this is just a loose wrapper around
|
|
# #OpenIDResponse.fields.
|
|
class OpenIDResponse
|
|
# The #OpenIDRequest I respond to.
|
|
attr_accessor :request
|
|
|
|
# An #OpenID::Message with the data to be returned.
|
|
# Keys are parameter names with no
|
|
# leading openid. e.g. identity and mac_key
|
|
# never openid.identity.
|
|
attr_accessor :fields
|
|
|
|
def initialize(request)
|
|
# Make a response to an OpenIDRequest.
|
|
@request = request
|
|
@fields = Message.new(request.namespace)
|
|
end
|
|
|
|
def to_s
|
|
return sprintf("%s for %s: %s",
|
|
self.class,
|
|
@request.class,
|
|
@fields)
|
|
end
|
|
|
|
# form_tag_attrs is a hash of attributes to be added to the form
|
|
# tag. 'accept-charset' and 'enctype' have defaults that can be
|
|
# overridden. If a value is supplied for 'action' or 'method',
|
|
# it will be replaced.
|
|
# Returns the form markup for this response.
|
|
def to_form_markup(form_tag_attrs=nil)
|
|
return @fields.to_form_markup(@request.return_to, form_tag_attrs)
|
|
end
|
|
|
|
# Wraps the form tag from to_form_markup in a complete HTML document
|
|
# that uses javascript to autosubmit the form.
|
|
def to_html(form_tag_attrs=nil)
|
|
return Util.auto_submit_html(to_form_markup(form_tag_attrs))
|
|
end
|
|
|
|
def render_as_form
|
|
# Returns true if this response's encoding is
|
|
# ENCODE_HTML_FORM. Convenience method for server authors.
|
|
return self.which_encoding == ENCODE_HTML_FORM
|
|
end
|
|
|
|
def needs_signing
|
|
# Does this response require signing?
|
|
return @fields.get_arg(OPENID_NS, 'mode') == 'id_res'
|
|
end
|
|
|
|
# implements IEncodable
|
|
|
|
def which_encoding
|
|
# How should I be encoded?
|
|
# returns one of ENCODE_URL or ENCODE_KVFORM.
|
|
if BROWSER_REQUEST_MODES.member?(@request.mode)
|
|
if @fields.is_openid2 and
|
|
encode_to_url.length > OPENID1_URL_LIMIT
|
|
return ENCODE_HTML_FORM
|
|
else
|
|
return ENCODE_URL
|
|
end
|
|
else
|
|
return ENCODE_KVFORM
|
|
end
|
|
end
|
|
|
|
def encode_to_url
|
|
# Encode a response as a URL for the user agent to GET.
|
|
# You will generally use this URL with a HTTP redirect.
|
|
return @fields.to_url(@request.return_to)
|
|
end
|
|
|
|
def add_extension(extension_response)
|
|
# Add an extension response to this response message.
|
|
#
|
|
# extension_response:: An object that implements the
|
|
# #OpenID::Extension interface for adding arguments to an OpenID
|
|
# message.
|
|
extension_response.to_message(@fields)
|
|
end
|
|
|
|
def encode_to_kvform
|
|
# Encode a response in key-value colon/newline format.
|
|
#
|
|
# This is a machine-readable format used to respond to
|
|
# messages which came directly from the consumer and not
|
|
# through the user agent.
|
|
#
|
|
# see: OpenID Specs,
|
|
# <a href="http://openid.net/specs.bml#keyvalue">Key-Value Colon/Newline format</a>
|
|
return @fields.to_kvform
|
|
end
|
|
|
|
def copy
|
|
return Marshal.load(Marshal.dump(self))
|
|
end
|
|
end
|
|
|
|
# I am a response to an OpenID request in terms a web server
|
|
# understands.
|
|
#
|
|
# I generally come from an #Encoder, either directly or from
|
|
# #Server.encodeResponse.
|
|
class WebResponse
|
|
|
|
# The HTTP code of this response as an integer.
|
|
attr_accessor :code
|
|
|
|
# #Hash of headers to include in this response.
|
|
attr_accessor :headers
|
|
|
|
# The body of this response.
|
|
attr_accessor :body
|
|
|
|
def initialize(code=HTTP_OK, headers=nil, body="")
|
|
# Construct me.
|
|
#
|
|
# These parameters are assigned directly as class attributes,
|
|
# see my class documentation for their
|
|
# descriptions.
|
|
@code = code
|
|
if headers
|
|
@headers = headers
|
|
else
|
|
@headers = {}
|
|
end
|
|
@body = body
|
|
end
|
|
end
|
|
|
|
# I sign things.
|
|
#
|
|
# I also check signatures.
|
|
#
|
|
# All my state is encapsulated in a store, which means I'm not
|
|
# generally pickleable but I am easy to reconstruct.
|
|
class Signatory
|
|
# The number of seconds a secret remains valid. Defaults to 14 days.
|
|
attr_accessor :secret_lifetime
|
|
|
|
# keys have a bogus server URL in them because the filestore
|
|
# really does expect that key to be a URL. This seems a little
|
|
# silly for the server store, since I expect there to be only
|
|
# one server URL.
|
|
@@_normal_key = 'http://localhost/|normal'
|
|
@@_dumb_key = 'http://localhost/|dumb'
|
|
|
|
def self._normal_key
|
|
@@_normal_key
|
|
end
|
|
|
|
def self._dumb_key
|
|
@@_dumb_key
|
|
end
|
|
|
|
attr_accessor :store
|
|
|
|
# Create a new Signatory. store is The back-end where my
|
|
# associations are stored.
|
|
def initialize(store)
|
|
Util.assert(store)
|
|
@store = store
|
|
@secret_lifetime = 14 * 24 * 60 * 60
|
|
end
|
|
|
|
# Verify that the signature for some data is valid.
|
|
def verify(assoc_handle, message)
|
|
assoc = get_association(assoc_handle, true)
|
|
if !assoc
|
|
Util.log(sprintf("failed to get assoc with handle %s to verify " +
|
|
"message %s", assoc_handle, message))
|
|
return false
|
|
end
|
|
|
|
begin
|
|
valid = assoc.check_message_signature(message)
|
|
rescue StandardError => ex
|
|
Util.log(sprintf("Error in verifying %s with %s: %s",
|
|
message, assoc, ex))
|
|
return false
|
|
end
|
|
|
|
return valid
|
|
end
|
|
|
|
# Sign a response.
|
|
#
|
|
# I take an OpenIDResponse, create a signature for everything in
|
|
# its signed list, and return a new copy of the response object
|
|
# with that signature included.
|
|
def sign(response)
|
|
signed_response = response.copy
|
|
assoc_handle = response.request.assoc_handle
|
|
if assoc_handle
|
|
# normal mode disabling expiration check because even if the
|
|
# association is expired, we still need to know some
|
|
# properties of the association so that we may preserve
|
|
# those properties when creating the fallback association.
|
|
assoc = get_association(assoc_handle, false, false)
|
|
|
|
if !assoc or assoc.expires_in <= 0
|
|
# fall back to dumb mode
|
|
signed_response.fields.set_arg(
|
|
OPENID_NS, 'invalidate_handle', assoc_handle)
|
|
assoc_type = assoc ? assoc.assoc_type : 'HMAC-SHA1'
|
|
if assoc and assoc.expires_in <= 0
|
|
# now do the clean-up that the disabled checkExpiration
|
|
# code didn't get to do.
|
|
invalidate(assoc_handle, false)
|
|
end
|
|
assoc = create_association(true, assoc_type)
|
|
end
|
|
else
|
|
# dumb mode.
|
|
assoc = create_association(true)
|
|
end
|
|
|
|
begin
|
|
signed_response.fields = assoc.sign_message(signed_response.fields)
|
|
rescue KVFormError => err
|
|
raise EncodingError, err
|
|
end
|
|
return signed_response
|
|
end
|
|
|
|
# Make a new association.
|
|
def create_association(dumb=true, assoc_type='HMAC-SHA1')
|
|
secret = CryptUtil.random_string(OpenID.get_secret_size(assoc_type))
|
|
uniq = Util.to_base64(CryptUtil.random_string(4))
|
|
handle = sprintf('{%s}{%x}{%s}', assoc_type, Time.now.to_i, uniq)
|
|
|
|
assoc = Association.from_expires_in(
|
|
secret_lifetime, handle, secret, assoc_type)
|
|
|
|
if dumb
|
|
key = @@_dumb_key
|
|
else
|
|
key = @@_normal_key
|
|
end
|
|
|
|
@store.store_association(key, assoc)
|
|
return assoc
|
|
end
|
|
|
|
# Get the association with the specified handle.
|
|
def get_association(assoc_handle, dumb, checkExpiration=true)
|
|
# Hmm. We've created an interface that deals almost entirely
|
|
# with assoc_handles. The only place outside the Signatory
|
|
# that uses this (and thus the only place that ever sees
|
|
# Association objects) is when creating a response to an
|
|
# association request, as it must have the association's
|
|
# secret.
|
|
|
|
if !assoc_handle
|
|
raise ArgumentError.new("assoc_handle must not be None")
|
|
end
|
|
|
|
if dumb
|
|
key = @@_dumb_key
|
|
else
|
|
key = @@_normal_key
|
|
end
|
|
|
|
assoc = @store.get_association(key, assoc_handle)
|
|
if assoc and assoc.expires_in <= 0
|
|
Util.log(sprintf("requested %sdumb key %s is expired (by %s seconds)",
|
|
(!dumb) ? 'not-' : '',
|
|
assoc_handle, assoc.expires_in))
|
|
if checkExpiration
|
|
@store.remove_association(key, assoc_handle)
|
|
assoc = nil
|
|
end
|
|
end
|
|
|
|
return assoc
|
|
end
|
|
|
|
# Invalidates the association with the given handle.
|
|
def invalidate(assoc_handle, dumb)
|
|
if dumb
|
|
key = @@_dumb_key
|
|
else
|
|
key = @@_normal_key
|
|
end
|
|
|
|
@store.remove_association(key, assoc_handle)
|
|
end
|
|
end
|
|
|
|
# I encode responses in to WebResponses.
|
|
#
|
|
# If you don't like WebResponses, you can do
|
|
# your own handling of OpenIDResponses with
|
|
# OpenIDResponse.whichEncoding,
|
|
# OpenIDResponse.encodeToURL, and
|
|
# OpenIDResponse.encodeToKVForm.
|
|
class Encoder
|
|
@@responseFactory = WebResponse
|
|
|
|
# Encode a response to a WebResponse.
|
|
#
|
|
# Raises EncodingError when I can't figure out how to encode
|
|
# this message.
|
|
def encode(response)
|
|
encode_as = response.which_encoding()
|
|
if encode_as == ENCODE_KVFORM
|
|
wr = @@responseFactory.new(HTTP_OK, nil,
|
|
response.encode_to_kvform())
|
|
if response.is_a?(Exception)
|
|
wr.code = HTTP_ERROR
|
|
end
|
|
elsif encode_as == ENCODE_URL
|
|
location = response.encode_to_url()
|
|
wr = @@responseFactory.new(HTTP_REDIRECT,
|
|
{'location' => location})
|
|
elsif encode_as == ENCODE_HTML_FORM
|
|
wr = @@responseFactory.new(HTTP_OK, nil,
|
|
response.to_form_markup())
|
|
else
|
|
# Can't encode this to a protocol message. You should
|
|
# probably render it to HTML and show it to the user.
|
|
raise EncodingError.new(response)
|
|
end
|
|
|
|
return wr
|
|
end
|
|
end
|
|
|
|
# I encode responses in to WebResponses, signing
|
|
# them when required.
|
|
class SigningEncoder < Encoder
|
|
|
|
attr_accessor :signatory
|
|
|
|
# Create a SigningEncoder given a Signatory
|
|
def initialize(signatory)
|
|
@signatory = signatory
|
|
end
|
|
|
|
# Encode a response to a WebResponse, signing it first if
|
|
# appropriate.
|
|
#
|
|
# Raises EncodingError when I can't figure out how to encode this
|
|
# message.
|
|
#
|
|
# Raises AlreadySigned when this response is already signed.
|
|
def encode(response)
|
|
# the is_a? is a bit of a kludge... it means there isn't
|
|
# really an adapter to make the interfaces quite match.
|
|
if !response.is_a?(Exception) and response.needs_signing()
|
|
if !@signatory
|
|
raise ArgumentError.new(
|
|
sprintf("Must have a store to sign this request: %s",
|
|
response), response)
|
|
end
|
|
|
|
if response.fields.has_key?(OPENID_NS, 'sig')
|
|
raise AlreadySigned.new(response)
|
|
end
|
|
|
|
response = @signatory.sign(response)
|
|
end
|
|
|
|
return super(response)
|
|
end
|
|
end
|
|
|
|
# I decode an incoming web request in to a OpenIDRequest.
|
|
class Decoder
|
|
|
|
@@handlers = {
|
|
'checkid_setup' => CheckIDRequest.method('from_message'),
|
|
'checkid_immediate' => CheckIDRequest.method('from_message'),
|
|
'check_authentication' => CheckAuthRequest.method('from_message'),
|
|
'associate' => AssociateRequest.method('from_message'),
|
|
}
|
|
|
|
attr_accessor :server
|
|
|
|
# Construct a Decoder. The server is necessary because some
|
|
# replies reference their server.
|
|
def initialize(server)
|
|
@server = server
|
|
end
|
|
|
|
# I transform query parameters into an OpenIDRequest.
|
|
#
|
|
# If the query does not seem to be an OpenID request at all, I
|
|
# return nil.
|
|
#
|
|
# Raises ProtocolError when the query does not seem to be a valid
|
|
# OpenID request.
|
|
def decode(query)
|
|
if query.nil? or query.length == 0
|
|
return nil
|
|
end
|
|
|
|
begin
|
|
message = Message.from_post_args(query)
|
|
rescue InvalidOpenIDNamespace => e
|
|
query = query.dup
|
|
query['openid.ns'] = OPENID2_NS
|
|
message = Message.from_post_args(query)
|
|
raise ProtocolError.new(message, e.to_s)
|
|
end
|
|
|
|
mode = message.get_arg(OPENID_NS, 'mode')
|
|
if !mode
|
|
msg = sprintf("No mode value in message %s", message)
|
|
raise ProtocolError.new(message, msg)
|
|
end
|
|
|
|
handler = @@handlers.fetch(mode, self.method('default_decoder'))
|
|
return handler.call(message, @server.op_endpoint)
|
|
end
|
|
|
|
# Called to decode queries when no handler for that mode is
|
|
# found.
|
|
#
|
|
# This implementation always raises ProtocolError.
|
|
def default_decoder(message, server)
|
|
mode = message.get_arg(OPENID_NS, 'mode')
|
|
msg = sprintf("Unrecognized OpenID mode %s", mode)
|
|
raise ProtocolError.new(message, msg)
|
|
end
|
|
end
|
|
|
|
# I handle requests for an OpenID server.
|
|
#
|
|
# Some types of requests (those which are not checkid requests)
|
|
# may be handed to my handleRequest method, and I will take care
|
|
# of it and return a response.
|
|
#
|
|
# For your convenience, I also provide an interface to
|
|
# Decoder.decode and SigningEncoder.encode through my methods
|
|
# decodeRequest and encodeResponse.
|
|
#
|
|
# All my state is encapsulated in an store, which means I'm not
|
|
# generally pickleable but I am easy to reconstruct.
|
|
class Server
|
|
@@signatoryClass = Signatory
|
|
@@encoderClass = SigningEncoder
|
|
@@decoderClass = Decoder
|
|
|
|
# The back-end where my associations and nonces are stored.
|
|
attr_accessor :store
|
|
|
|
# I'm using this for associate requests and to sign things.
|
|
attr_accessor :signatory
|
|
|
|
# I'm using this to encode things.
|
|
attr_accessor :encoder
|
|
|
|
# I'm using this to decode things.
|
|
attr_accessor :decoder
|
|
|
|
# I use this instance of OpenID::AssociationNegotiator to
|
|
# determine which kinds of associations I can make and how.
|
|
attr_accessor :negotiator
|
|
|
|
# My URL.
|
|
attr_accessor :op_endpoint
|
|
|
|
# op_endpoint is new in library version 2.0.
|
|
def initialize(store, op_endpoint)
|
|
@store = store
|
|
@signatory = @@signatoryClass.new(@store)
|
|
@encoder = @@encoderClass.new(@signatory)
|
|
@decoder = @@decoderClass.new(self)
|
|
@negotiator = DefaultNegotiator.copy()
|
|
@op_endpoint = op_endpoint
|
|
end
|
|
|
|
# Handle a request.
|
|
#
|
|
# Give me a request, I will give you a response. Unless it's a
|
|
# type of request I cannot handle myself, in which case I will
|
|
# raise RuntimeError. In that case, you can handle it yourself,
|
|
# or add a method to me for handling that request type.
|
|
def handle_request(request)
|
|
begin
|
|
handler = self.method('openid_' + request.mode)
|
|
rescue NameError
|
|
raise RuntimeError.new(
|
|
sprintf("%s has no handler for a request of mode %s.",
|
|
self, request.mode))
|
|
end
|
|
|
|
return handler.call(request)
|
|
end
|
|
|
|
# Handle and respond to check_authentication requests.
|
|
def openid_check_authentication(request)
|
|
return request.answer(@signatory)
|
|
end
|
|
|
|
# Handle and respond to associate requests.
|
|
def openid_associate(request)
|
|
assoc_type = request.assoc_type
|
|
session_type = request.session.session_type
|
|
if @negotiator.allowed?(assoc_type, session_type)
|
|
assoc = @signatory.create_association(false,
|
|
assoc_type)
|
|
return request.answer(assoc)
|
|
else
|
|
message = sprintf('Association type %s is not supported with ' +
|
|
'session type %s', assoc_type, session_type)
|
|
preferred_assoc_type, preferred_session_type = @negotiator.get_allowed_type()
|
|
return request.answer_unsupported(message,
|
|
preferred_assoc_type,
|
|
preferred_session_type)
|
|
end
|
|
end
|
|
|
|
# Transform query parameters into an OpenIDRequest.
|
|
# query should contain the query parameters as a Hash with
|
|
# each key mapping to one value.
|
|
#
|
|
# If the query does not seem to be an OpenID request at all, I
|
|
# return nil.
|
|
def decode_request(query)
|
|
return @decoder.decode(query)
|
|
end
|
|
|
|
# Encode a response to a WebResponse, signing it first if
|
|
# appropriate.
|
|
#
|
|
# Raises EncodingError when I can't figure out how to encode this
|
|
# message.
|
|
#
|
|
# Raises AlreadySigned When this response is already signed.
|
|
def encode_response(response)
|
|
return @encoder.encode(response)
|
|
end
|
|
end
|
|
|
|
# A message did not conform to the OpenID protocol.
|
|
class ProtocolError < Exception
|
|
# The query that is failing to be a valid OpenID request.
|
|
attr_accessor :openid_message
|
|
attr_accessor :reference
|
|
attr_accessor :contact
|
|
|
|
# text:: A message about the encountered error.
|
|
def initialize(message, text=nil, reference=nil, contact=nil)
|
|
@openid_message = message
|
|
@reference = reference
|
|
@contact = contact
|
|
Util.assert(!message.is_a?(String))
|
|
super(text)
|
|
end
|
|
|
|
# Get the return_to argument from the request, if any.
|
|
def get_return_to
|
|
if @openid_message.nil?
|
|
return nil
|
|
else
|
|
return @openid_message.get_arg(OPENID_NS, 'return_to')
|
|
end
|
|
end
|
|
|
|
# Did this request have a return_to parameter?
|
|
def has_return_to
|
|
return !get_return_to.nil?
|
|
end
|
|
|
|
# Generate a Message object for sending to the relying party,
|
|
# after encoding.
|
|
def to_message
|
|
namespace = @openid_message.get_openid_namespace()
|
|
reply = Message.new(namespace)
|
|
reply.set_arg(OPENID_NS, 'mode', 'error')
|
|
reply.set_arg(OPENID_NS, 'error', self.to_s)
|
|
|
|
if @contact
|
|
reply.set_arg(OPENID_NS, 'contact', @contact.to_s)
|
|
end
|
|
|
|
if @reference
|
|
reply.set_arg(OPENID_NS, 'reference', @reference.to_s)
|
|
end
|
|
|
|
return reply
|
|
end
|
|
|
|
# implements IEncodable
|
|
|
|
def encode_to_url
|
|
return to_message().to_url(get_return_to())
|
|
end
|
|
|
|
def encode_to_kvform
|
|
return to_message().to_kvform()
|
|
end
|
|
|
|
def to_form_markup
|
|
return to_message().to_form_markup(get_return_to())
|
|
end
|
|
|
|
def to_html
|
|
return Util.auto_submit_html(to_form_markup)
|
|
end
|
|
|
|
# How should I be encoded?
|
|
#
|
|
# Returns one of ENCODE_URL, ENCODE_KVFORM, or None. If None,
|
|
# I cannot be encoded as a protocol message and should be
|
|
# displayed to the user.
|
|
def which_encoding
|
|
if has_return_to()
|
|
if @openid_message.is_openid2 and
|
|
encode_to_url().length > OPENID1_URL_LIMIT
|
|
return ENCODE_HTML_FORM
|
|
else
|
|
return ENCODE_URL
|
|
end
|
|
end
|
|
|
|
if @openid_message.nil?
|
|
return nil
|
|
end
|
|
|
|
mode = @openid_message.get_arg(OPENID_NS, 'mode')
|
|
if mode
|
|
if !BROWSER_REQUEST_MODES.member?(mode)
|
|
return ENCODE_KVFORM
|
|
end
|
|
end
|
|
|
|
# If your request was so broken that you didn't manage to
|
|
# include an openid.mode, I'm not going to worry too much
|
|
# about returning you something you can't parse.
|
|
return nil
|
|
end
|
|
end
|
|
|
|
# Raised when an operation was attempted that is not compatible
|
|
# with the protocol version being used.
|
|
class VersionError < Exception
|
|
end
|
|
|
|
# Raised when a response to a request cannot be generated
|
|
# because the request contains no return_to URL.
|
|
class NoReturnToError < Exception
|
|
end
|
|
|
|
# Could not encode this as a protocol message.
|
|
#
|
|
# You should probably render it and show it to the user.
|
|
class EncodingError < Exception
|
|
# The response that failed to encode.
|
|
attr_reader :response
|
|
|
|
def initialize(response)
|
|
super(response)
|
|
@response = response
|
|
end
|
|
end
|
|
|
|
# This response is already signed.
|
|
class AlreadySigned < EncodingError
|
|
end
|
|
|
|
# A return_to is outside the trust_root.
|
|
class UntrustedReturnURL < ProtocolError
|
|
attr_reader :return_to, :trust_root
|
|
|
|
def initialize(message, return_to, trust_root)
|
|
super(message)
|
|
@return_to = return_to
|
|
@trust_root = trust_root
|
|
end
|
|
|
|
def to_s
|
|
return sprintf("return_to %s not under trust_root %s",
|
|
@return_to,
|
|
@trust_root)
|
|
end
|
|
end
|
|
|
|
# The return_to URL doesn't look like a valid URL.
|
|
class MalformedReturnURL < ProtocolError
|
|
attr_reader :return_to
|
|
|
|
def initialize(openid_message, return_to)
|
|
@return_to = return_to
|
|
super(openid_message)
|
|
end
|
|
end
|
|
|
|
# The trust root is not well-formed.
|
|
class MalformedTrustRoot < ProtocolError
|
|
end
|
|
end
|
|
end
|