server.rb
1544 lines
| 49.7 KiB
| text/x-ruby
|
RubyLexer
|
r2376 | |||
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 | ||||