C0 code coverage information

Generated on Fri Jul 11 15:55:34 -0700 2008 with rcov 0.7.0


Code reported as executed by Ruby looks like this...
and this: this line is also marked as covered.
Lines considered as run by rcov, but not reported by Ruby, look like this,
and this: these lines were inferred by rcov (using simple heuristics).
Finally, here's a line marked as not executed.
Name Total lines Lines of code Total coverage Code coverage
lib/openid/server.rb 1544 907
99.6% 
99.3% 
   1 
   2 require 'openid/cryptutil'
   3 require 'openid/util'
   4 require 'openid/dh'
   5 require 'openid/store/nonce'
   6 require 'openid/trustroot'
   7 require 'openid/association'
   8 require 'openid/message'
   9 
  10 require 'time'
  11 
  12 module OpenID
  13 
  14   module Server
  15 
  16     HTTP_OK = 200
  17     HTTP_REDIRECT = 302
  18     HTTP_ERROR = 400
  19 
  20     BROWSER_REQUEST_MODES = ['checkid_setup', 'checkid_immediate']
  21 
  22     ENCODE_KVFORM = ['kvform'].freeze
  23     ENCODE_URL = ['URL/redirect'].freeze
  24     ENCODE_HTML_FORM = ['HTML form'].freeze
  25 
  26     UNUSED = nil
  27 
  28     class OpenIDRequest
  29       attr_accessor :message, :mode
  30 
  31       # I represent an incoming OpenID request.
  32       #
  33       # Attributes:
  34       # mode:: The "openid.mode" of this request
  35       def initialize
  36         @mode = nil
  37         @message = nil
  38       end
  39 
  40       def namespace
  41         if @message.nil?
  42           raise RuntimeError, "Request has no message"
  43         else
  44           return @message.get_openid_namespace
  45         end
  46       end
  47     end
  48 
  49     # A request to verify the validity of a previous response.
  50     #
  51     # See OpenID Specs, Verifying Directly with the OpenID Provider
  52     # <http://openid.net/specs/openid-authentication-2_0-12.html#verifying_signatures>
  53     class CheckAuthRequest < OpenIDRequest
  54 
  55       # The association handle the response was signed with.
  56       attr_accessor :assoc_handle
  57 
  58       # The message with the signature which wants checking.
  59       attr_accessor :signed
  60 
  61       # An association handle the client is asking about the validity
  62       # of. May be nil.
  63       attr_accessor :invalidate_handle
  64 
  65       attr_accessor :sig
  66 
  67       # Construct me.
  68       #
  69       # These parameters are assigned directly as class attributes.
  70       #
  71       # Parameters:
  72       # assoc_handle:: the association handle for this request
  73       # signed:: The signed message
  74       # invalidate_handle:: An association handle that the relying
  75       #                     party is checking to see if it is invalid
  76       def initialize(assoc_handle, signed, invalidate_handle=nil)
  77         super()
  78 
  79         @mode = "check_authentication"
  80         @required_fields = ["identity", "return_to", "response_nonce"].freeze
  81 
  82         @sig = nil
  83         @assoc_handle = assoc_handle
  84         @signed = signed
  85         @invalidate_handle = invalidate_handle
  86       end
  87 
  88       # Construct me from an OpenID::Message.
  89       def self.from_message(message, op_endpoint=UNUSED)
  90         assoc_handle = message.get_arg(OPENID_NS, 'assoc_handle')
  91         invalidate_handle = message.get_arg(OPENID_NS, 'invalidate_handle')
  92 
  93         signed = message.copy()
  94         # openid.mode is currently check_authentication because
  95         # that's the mode of this request.  But the signature
  96         # was made on something with a different openid.mode.
  97         # http://article.gmane.org/gmane.comp.web.openid.general/537
  98         if signed.has_key?(OPENID_NS, "mode")
  99           signed.set_arg(OPENID_NS, "mode", "id_res")
 100         end
 101 
 102         obj = self.new(assoc_handle, signed, invalidate_handle)
 103         obj.message = message
 104         obj.sig = message.get_arg(OPENID_NS, 'sig')
 105 
 106         if !obj.assoc_handle or
 107             !obj.sig
 108           msg = sprintf("%s request missing required parameter from message %s",
 109                         obj.mode, message)
 110             raise ProtocolError.new(message, msg)
 111         end
 112 
 113         return obj
 114       end
 115 
 116       # Respond to this request.
 117       #
 118       # Given a Signatory, I can check the validity of the signature
 119       # and the invalidate_handle.  I return a response with an
 120       # is_valid (and, if appropriate invalidate_handle) field.
 121       def answer(signatory)
 122         is_valid = signatory.verify(@assoc_handle, @signed)
 123         # Now invalidate that assoc_handle so it this checkAuth
 124         # message cannot be replayed.
 125         signatory.invalidate(@assoc_handle, dumb=true)
 126         response = OpenIDResponse.new(self)
 127         valid_str = is_valid ? "true" : "false"
 128         response.fields.set_arg(OPENID_NS, 'is_valid', valid_str)
 129 
 130         if @invalidate_handle
 131           assoc = signatory.get_association(@invalidate_handle, false)
 132           if !assoc
 133             response.fields.set_arg(
 134                     OPENID_NS, 'invalidate_handle', @invalidate_handle)
 135           end
 136         end
 137 
 138         return response
 139       end
 140 
 141       def to_s
 142         ih = nil
 143 
 144         if @invalidate_handle
 145           ih = sprintf(" invalidate? %s", @invalidate_handle)
 146         else
 147           ih = ""
 148         end
 149 
 150         s = sprintf("<%s handle: %s sig: %s: signed: %s%s>",
 151                     self.class, @assoc_handle,
 152                     @sig, @signed, ih)
 153         return s
 154       end
 155     end
 156 
 157     class BaseServerSession
 158       attr_reader :session_type
 159 
 160       def initialize(session_type, allowed_assoc_types)
 161         @session_type = session_type
 162         @allowed_assoc_types = allowed_assoc_types.dup.freeze
 163       end
 164 
 165       def allowed_assoc_type?(typ)
 166         @allowed_assoc_types.member?(typ)
 167       end
 168     end
 169 
 170     # An object that knows how to handle association requests with
 171     # no session type.
 172     #
 173     # See OpenID Specs, Section 8: Establishing Associations
 174     # <http://openid.net/specs/openid-authentication-2_0-12.html#associations>
 175     class PlainTextServerSession < BaseServerSession
 176       # The session_type for this association session. There is no
 177       # type defined for plain-text in the OpenID specification, so we
 178       # use 'no-encryption'.
 179       attr_reader :session_type
 180 
 181       def initialize
 182         super('no-encryption', ['HMAC-SHA1', 'HMAC-SHA256'])
 183       end
 184 
 185       def self.from_message(unused_request)
 186         return self.new
 187       end
 188 
 189       def answer(secret)
 190         return {'mac_key' => Util.to_base64(secret)}
 191       end
 192     end
 193 
 194     # An object that knows how to handle association requests with the
 195     # Diffie-Hellman session type.
 196     #
 197     # See OpenID Specs, Section 8: Establishing Associations
 198     # <http://openid.net/specs/openid-authentication-2_0-12.html#associations>
 199     class DiffieHellmanSHA1ServerSession < BaseServerSession
 200 
 201       # The Diffie-Hellman algorithm values for this request
 202       attr_accessor :dh
 203 
 204       # The public key sent by the consumer in the associate request
 205       attr_accessor :consumer_pubkey
 206 
 207       # The session_type for this association session.
 208       attr_reader :session_type
 209 
 210       def initialize(dh, consumer_pubkey)
 211         super('DH-SHA1', ['HMAC-SHA1'])
 212 
 213         @hash_func = CryptUtil.method('sha1')
 214         @dh = dh
 215         @consumer_pubkey = consumer_pubkey
 216       end
 217 
 218       # Construct me from OpenID Message
 219       #
 220       # Raises ProtocolError when parameters required to establish the
 221       # session are missing.
 222       def self.from_message(message)
 223         dh_modulus = message.get_arg(OPENID_NS, 'dh_modulus')
 224         dh_gen = message.get_arg(OPENID_NS, 'dh_gen')
 225         if ((!dh_modulus and dh_gen) or
 226             (!dh_gen and dh_modulus))
 227 
 228           if !dh_modulus
 229             missing = 'modulus'
 230           else
 231             missing = 'generator'
 232           end
 233 
 234           raise ProtocolError.new(message,
 235                   sprintf('If non-default modulus or generator is ' +
 236                           'supplied, both must be supplied. Missing %s',
 237                           missing))
 238         end
 239 
 240         if dh_modulus or dh_gen
 241           dh_modulus = CryptUtil.base64_to_num(dh_modulus)
 242           dh_gen = CryptUtil.base64_to_num(dh_gen)
 243           dh = DiffieHellman.new(dh_modulus, dh_gen)
 244         else
 245           dh = DiffieHellman.from_defaults()
 246         end
 247 
 248         consumer_pubkey = message.get_arg(OPENID_NS, 'dh_consumer_public')
 249         if !consumer_pubkey
 250           raise ProtocolError.new(message,
 251                   sprintf("Public key for DH-SHA1 session " +
 252                           "not found in message %s", message))
 253         end
 254 
 255         consumer_pubkey = CryptUtil.base64_to_num(consumer_pubkey)
 256 
 257         return self.new(dh, consumer_pubkey)
 258       end
 259 
 260       def answer(secret)
 261         mac_key = @dh.xor_secret(@hash_func,
 262                                  @consumer_pubkey,
 263                                  secret)
 264         return {
 265             'dh_server_public' => CryptUtil.num_to_base64(@dh.public),
 266             'enc_mac_key' => Util.to_base64(mac_key),
 267             }
 268       end
 269     end
 270 
 271     class DiffieHellmanSHA256ServerSession < DiffieHellmanSHA1ServerSession
 272       def initialize(*args)
 273         super(*args)
 274         @session_type = 'DH-SHA256'
 275         @hash_func = CryptUtil.method('sha256')
 276         @allowed_assoc_types = ['HMAC-SHA256'].freeze
 277       end
 278     end
 279 
 280     # A request to establish an association.
 281     #
 282     # See OpenID Specs, Section 8: Establishing Associations
 283     # <http://openid.net/specs/openid-authentication-2_0-12.html#associations>
 284     class AssociateRequest < OpenIDRequest
 285       # An object that knows how to handle association requests of a
 286       # certain type.
 287       attr_accessor :session
 288 
 289       # The type of association. Supported values include HMAC-SHA256
 290       # and HMAC-SHA1
 291       attr_accessor :assoc_type
 292 
 293       @@session_classes = {
 294         'no-encryption' => PlainTextServerSession,
 295         'DH-SHA1' => DiffieHellmanSHA1ServerSession,
 296         'DH-SHA256' => DiffieHellmanSHA256ServerSession,
 297       }
 298 
 299       # Construct me.
 300       #
 301       # The session is assigned directly as a class attribute. See my
 302       # class documentation for its description.
 303       def initialize(session, assoc_type)
 304         super()
 305         @session = session
 306         @assoc_type = assoc_type
 307 
 308         @mode = "associate"
 309       end
 310 
 311       # Construct me from an OpenID Message.
 312       def self.from_message(message, op_endpoint=UNUSED)
 313         if message.is_openid1()
 314           session_type = message.get_arg(OPENID_NS, 'session_type')
 315           if session_type == 'no-encryption'
 316             Util.log('Received OpenID 1 request with a no-encryption ' +
 317                      'association session type. Continuing anyway.')
 318           elsif !session_type
 319             session_type = 'no-encryption'
 320           end
 321         else
 322           session_type = message.get_arg(OPENID2_NS, 'session_type')
 323           if !session_type
 324             raise ProtocolError.new(message,
 325                                     text="session_type missing from request")
 326           end
 327         end
 328 
 329         session_class = @@session_classes[session_type]
 330 
 331         if !session_class
 332           raise ProtocolError.new(message,
 333                   sprintf("Unknown session type %s", session_type))
 334         end
 335 
 336         begin
 337           session = session_class.from_message(message)
 338         rescue ArgumentError => why
 339           # XXX
 340           raise ProtocolError.new(message,
 341                                   sprintf('Error parsing %s session: %s',
 342                                           session_type, why))
 343         end
 344 
 345         assoc_type = message.get_arg(OPENID_NS, 'assoc_type', 'HMAC-SHA1')
 346         if !session.allowed_assoc_type?(assoc_type)
 347           msg = sprintf('Session type %s does not support association type %s',
 348                         session_type, assoc_type)
 349           raise ProtocolError.new(message, msg)
 350         end
 351 
 352         obj = self.new(session, assoc_type)
 353         obj.message = message
 354         return obj
 355       end
 356 
 357       # Respond to this request with an association.
 358       #
 359       # assoc:: The association to send back.
 360       #
 361       # Returns a response with the association information, encrypted
 362       # to the consumer's public key if appropriate.
 363       def answer(assoc)
 364         response = OpenIDResponse.new(self)
 365         response.fields.update_args(OPENID_NS, {
 366             'expires_in' => sprintf('%d', assoc.expires_in()),
 367             'assoc_type' => @assoc_type,
 368             'assoc_handle' => assoc.handle,
 369             })
 370         response.fields.update_args(OPENID_NS,
 371                                    @session.answer(assoc.secret))
 372         unless (@session.session_type == 'no-encryption' and
 373                 @message.is_openid1)
 374           response.fields.set_arg(
 375               OPENID_NS, 'session_type', @session.session_type)
 376         end
 377 
 378         return response
 379       end
 380 
 381       # Respond to this request indicating that the association type
 382       # or association session type is not supported.
 383       def answer_unsupported(message, preferred_association_type=nil,
 384                              preferred_session_type=nil)
 385         if @message.is_openid1()
 386           raise ProtocolError.new(@message)
 387         end
 388 
 389         response = OpenIDResponse.new(self)
 390         response.fields.set_arg(OPENID_NS, 'error_code', 'unsupported-type')
 391         response.fields.set_arg(OPENID_NS, 'error', message)
 392 
 393         if preferred_association_type
 394           response.fields.set_arg(
 395               OPENID_NS, 'assoc_type', preferred_association_type)
 396         end
 397 
 398         if preferred_session_type
 399           response.fields.set_arg(
 400               OPENID_NS, 'session_type', preferred_session_type)
 401         end
 402 
 403         return response
 404       end
 405     end
 406 
 407     # A request to confirm the identity of a user.
 408     #
 409     # This class handles requests for openid modes
 410     # +checkid_immediate+ and +checkid_setup+ .
 411     class CheckIDRequest < OpenIDRequest
 412 
 413       # Provided in smart mode requests, a handle for a previously
 414       # established association.  nil for dumb mode requests.
 415       attr_accessor :assoc_handle
 416 
 417       # Is this an immediate-mode request?
 418       attr_accessor :immediate
 419 
 420       # The URL to send the user agent back to to reply to this
 421       # request.
 422       attr_accessor :return_to
 423 
 424       # The OP-local identifier being checked.
 425       attr_accessor :identity
 426 
 427       # The claimed identifier.  Not present in OpenID 1.x
 428       # messages.
 429       attr_accessor :claimed_id
 430 
 431       # This URL identifies the party making the request, and the user
 432       # will use that to make her decision about what answer she
 433       # trusts them to have. Referred to as "realm" in OpenID 2.0.
 434       attr_accessor :trust_root
 435 
 436       # mode:: +checkid_immediate+ or +checkid_setup+
 437       attr_accessor :mode
 438 
 439       attr_accessor :op_endpoint
 440 
 441       # These parameters are assigned directly as attributes,
 442       # see the #CheckIDRequest class documentation for their
 443       # descriptions.
 444       #
 445       # Raises #MalformedReturnURL when the +return_to+ URL is not
 446       # a URL.
 447       def initialize(identity, return_to, op_endpoint, trust_root=nil,
 448                      immediate=false, assoc_handle=nil, claimed_id=nil)
 449         @assoc_handle = assoc_handle
 450         @identity = identity
 451         @claimed_id = (claimed_id or identity)
 452         @return_to = return_to
 453         @trust_root = (trust_root or return_to)
 454         @op_endpoint = op_endpoint
 455         @message = nil
 456 
 457         if immediate
 458           @immediate = true
 459           @mode = "checkid_immediate"
 460         else
 461           @immediate = false
 462           @mode = "checkid_setup"
 463         end
 464 
 465         if @return_to and
 466             !TrustRoot::TrustRoot.parse(@return_to)
 467           raise MalformedReturnURL.new(nil, @return_to)
 468         end
 469 
 470         if !trust_root_valid()
 471           raise UntrustedReturnURL.new(nil, @return_to, @trust_root)
 472         end
 473       end
 474 
 475       # Construct me from an OpenID message.
 476       #
 477       # message:: An OpenID checkid_* request Message
 478       #
 479       # op_endpoint:: The endpoint URL of the server that this
 480       #               message was sent to.
 481       #
 482       # Raises:
 483       # ProtocolError:: When not all required parameters are present
 484       #                 in the message.
 485       #
 486       # MalformedReturnURL:: When the +return_to+ URL is not a URL.
 487       #
 488       # UntrustedReturnURL:: When the +return_to+ URL is
 489       #                      outside the +trust_root+.
 490       def self.from_message(message, op_endpoint)
 491         obj = self.allocate
 492         obj.message = message
 493         obj.op_endpoint = op_endpoint
 494         mode = message.get_arg(OPENID_NS, 'mode')
 495         if mode == "checkid_immediate"
 496           obj.immediate = true
 497           obj.mode = "checkid_immediate"
 498         else
 499           obj.immediate = false
 500           obj.mode = "checkid_setup"
 501         end
 502 
 503         obj.return_to = message.get_arg(OPENID_NS, 'return_to')
 504         if message.is_openid1 and !obj.return_to
 505           msg = sprintf("Missing required field 'return_to' from %s",
 506                         message)
 507           raise ProtocolError.new(message, msg)
 508         end
 509 
 510         obj.identity = message.get_arg(OPENID_NS, 'identity')
 511         obj.claimed_id = message.get_arg(OPENID_NS, 'claimed_id')
 512         if message.is_openid1()
 513           if !obj.identity
 514             s = "OpenID 1 message did not contain openid.identity"
 515             raise ProtocolError.new(message, s)
 516           end
 517         else
 518           if obj.identity and not obj.claimed_id
 519             s = ("OpenID 2.0 message contained openid.identity but not " +
 520                  "claimed_id")
 521             raise ProtocolError.new(message, s)
 522           elsif obj.claimed_id and not obj.identity
 523             s = ("OpenID 2.0 message contained openid.claimed_id but not " +
 524                  "identity")
 525             raise ProtocolError.new(message, s)
 526           end
 527         end
 528 
 529         # There's a case for making self.trust_root be a TrustRoot
 530         # here.  But if TrustRoot isn't currently part of the "public"
 531         # API, I'm not sure it's worth doing.
 532         if message.is_openid1
 533           trust_root_param = 'trust_root'
 534         else
 535           trust_root_param = 'realm'
 536         end
 537         trust_root = message.get_arg(OPENID_NS, trust_root_param)
 538         trust_root = obj.return_to if (trust_root.nil? || trust_root.empty?)
 539         obj.trust_root = trust_root
 540 
 541         if !message.is_openid1 and !obj.return_to and !obj.trust_root
 542           raise ProtocolError.new(message, "openid.realm required when " +
 543                                   "openid.return_to absent")
 544         end
 545 
 546         obj.assoc_handle = message.get_arg(OPENID_NS, 'assoc_handle')
 547 
 548         # Using TrustRoot.parse here is a bit misleading, as we're not
 549         # parsing return_to as a trust root at all.  However, valid
 550         # URLs are valid trust roots, so we can use this to get an
 551         # idea if it is a valid URL.  Not all trust roots are valid
 552         # return_to URLs, however (particularly ones with wildcards),
 553         # so this is still a little sketchy.
 554         if obj.return_to and \
 555           !TrustRoot::TrustRoot.parse(obj.return_to)
 556           raise MalformedReturnURL.new(message, obj.return_to)
 557         end
 558 
 559         # I first thought that checking to see if the return_to is
 560         # within the trust_root is premature here, a
 561         # logic-not-decoding thing.  But it was argued that this is
 562         # really part of data validation.  A request with an invalid
 563         # trust_root/return_to is broken regardless of application,
 564         # right?
 565         if !obj.trust_root_valid()
 566           raise UntrustedReturnURL.new(message, obj.return_to, obj.trust_root)
 567         end
 568 
 569         return obj
 570       end
 571 
 572       # Is the identifier to be selected by the IDP?
 573       def id_select
 574         # So IDPs don't have to import the constant
 575         return @identity == IDENTIFIER_SELECT
 576       end
 577 
 578       # Is my return_to under my trust_root?
 579       def trust_root_valid
 580         if !@trust_root
 581           return true
 582         end
 583 
 584         tr = TrustRoot::TrustRoot.parse(@trust_root)
 585         if !tr
 586           raise MalformedTrustRoot.new(@message, @trust_root)
 587         end
 588 
 589         if @return_to
 590           return tr.validate_url(@return_to)
 591         else
 592           return true
 593         end
 594       end
 595 
 596       # Does the relying party publish the return_to URL for this
 597       # response under the realm? It is up to the provider to set a
 598       # policy for what kinds of realms should be allowed. This
 599       # return_to URL verification reduces vulnerability to
 600       # data-theft attacks based on open proxies,
 601       # corss-site-scripting, or open redirectors.
 602       #
 603       # This check should only be performed after making sure that
 604       # the return_to URL matches the realm.
 605       #
 606       # Raises DiscoveryFailure if the realm
 607       # URL does not support Yadis discovery (and so does not
 608       # support the verification process).
 609       #
 610       # Returns true if the realm publishes a document with the
 611       # return_to URL listed
 612       def return_to_verified
 613         return TrustRoot.verify_return_to(@trust_root, @return_to)
 614       end
 615 
 616       # Respond to this request.
 617       #
 618       # allow:: Allow this user to claim this identity, and allow the
 619       #         consumer to have this information?
 620       #
 621       # server_url:: DEPRECATED.  Passing op_endpoint to the
 622       #              #Server constructor makes this optional.
 623       #
 624       #              When an OpenID 1.x immediate mode request does
 625       #              not succeed, it gets back a URL where the request
 626       #              may be carried out in a not-so-immediate fashion.
 627       #              Pass my URL in here (the fully qualified address
 628       #              of this server's endpoint, i.e.
 629       #              <tt>http://example.com/server</tt>), and I will
 630       #              use it as a base for the URL for a new request.
 631       #
 632       #              Optional for requests where
 633       #              #CheckIDRequest.immediate is false or +allow+ is
 634       #              true.
 635       #
 636       # identity:: The OP-local identifier to answer with.  Only for use
 637       #            when the relying party requested identifier selection.
 638       #
 639       # claimed_id:: The claimed identifier to answer with,
 640       #              for use with identifier selection in the case where the
 641       #              claimed identifier and the OP-local identifier differ,
 642       #              i.e. when the claimed_id uses delegation.
 643       #
 644       #              If +identity+ is provided but this is not,
 645       #              +claimed_id+ will default to the value of +identity+.
 646       #              When answering requests that did not ask for identifier
 647       #              selection, the response +claimed_id+ will default to
 648       #              that of the request.
 649       #
 650       #              This parameter is new in OpenID 2.0.
 651       #
 652       # Returns an OpenIDResponse object containing a OpenID id_res message.
 653       #
 654       # Raises NoReturnToError if the return_to is missing.
 655       #
 656       # Version 2.0 deprecates +server_url+ and adds +claimed_id+.
 657       def answer(allow, server_url=nil, identity=nil, claimed_id=nil)
 658         if !@return_to
 659           raise NoReturnToError
 660         end
 661 
 662         if !server_url
 663           if @message.is_openid2 and !@op_endpoint
 664             # In other words, that warning I raised in
 665             # Server.__init__?  You should pay attention to it now.
 666             raise RuntimeError, ("#{self} should be constructed with "\
 667                                  "op_endpoint to respond to OpenID 2.0 "\
 668                                  "messages.")
 669           end
 670 
 671           server_url = @op_endpoint
 672         end
 673 
 674         if allow
 675           mode = 'id_res'
 676         elsif @message.is_openid1
 677           if @immediate
 678             mode = 'id_res'
 679           else
 680             mode = 'cancel'
 681           end
 682         else
 683           if @immediate
 684             mode = 'setup_needed'
 685           else
 686             mode = 'cancel'
 687           end
 688         end
 689 
 690         response = OpenIDResponse.new(self)
 691 
 692         if claimed_id and @message.is_openid1
 693           raise VersionError, ("claimed_id is new in OpenID 2.0 and not "\
 694                                "available for #{@message.get_openid_namespace}")
 695         end
 696 
 697         if identity and !claimed_id
 698           claimed_id = identity
 699         end
 700 
 701         if allow
 702           if @identity == IDENTIFIER_SELECT
 703             if !identity
 704               raise ArgumentError, ("This request uses IdP-driven "\
 705                                     "identifier selection.You must supply "\
 706                                     "an identifier in the response.")
 707             end
 708 
 709             response_identity = identity
 710             response_claimed_id = claimed_id
 711 
 712           elsif @identity
 713             if identity and (@identity != identity)
 714               raise ArgumentError, ("Request was for identity #{@identity}, "\
 715                                     "cannot reply with identity #{identity}")
 716             end
 717 
 718             response_identity = @identity
 719             response_claimed_id = @claimed_id
 720           else
 721             if identity
 722               raise ArgumentError, ("This request specified no identity "\
 723                                     "and you supplied #{identity}")
 724             end
 725             response_identity = nil
 726           end
 727 
 728           if @message.is_openid1 and !response_identity
 729             raise ArgumentError, ("Request was an OpenID 1 request, so "\
 730                                   "response must include an identifier.")
 731           end
 732 
 733           response.fields.update_args(OPENID_NS, {
 734                 'mode' => mode,
 735                 'op_endpoint' => server_url,
 736                 'return_to' => @return_to,
 737                 'response_nonce' => Nonce.mk_nonce(),
 738                 })
 739 
 740           if response_identity
 741             response.fields.set_arg(OPENID_NS, 'identity', response_identity)
 742             if @message.is_openid2
 743               response.fields.set_arg(OPENID_NS,
 744                                       'claimed_id', response_claimed_id)
 745             end
 746           end
 747         else
 748           response.fields.set_arg(OPENID_NS, 'mode', mode)
 749           if @immediate
 750             if @message.is_openid1 and !server_url
 751               raise ArgumentError, ("setup_url is required for allow=false "\
 752                                     "in OpenID 1.x immediate mode.")
 753             end
 754 
 755             # Make a new request just like me, but with
 756             # immediate=false.
 757             setup_request = self.class.new(@identity, @return_to,
 758                                            @op_endpoint, @trust_root, false,
 759                                            @assoc_handle, @claimed_id)
 760             setup_request.message = Message.new(@message.get_openid_namespace)
 761             setup_url = setup_request.encode_to_url(server_url)
 762             response.fields.set_arg(OPENID_NS, 'user_setup_url', setup_url)
 763           end
 764         end
 765 
 766         return response
 767       end
 768 
 769       def encode_to_url(server_url)
 770         # Encode this request as a URL to GET.
 771         #
 772         # server_url:: The URL of the OpenID server to make this
 773         #              request of.
 774         if !@return_to
 775           raise NoReturnToError
 776         end
 777 
 778         # Imported from the alternate reality where these classes are
 779         # used in both the client and server code, so Requests are
 780         # Encodable too.  That's right, code imported from alternate
 781         # realities all for the love of you, id_res/user_setup_url.
 782         q = {'mode' => @mode,
 783              'identity' => @identity,
 784              'claimed_id' => @claimed_id,
 785              'return_to' => @return_to}
 786 
 787         if @trust_root
 788           if @message.is_openid1
 789             q['trust_root'] = @trust_root
 790           else
 791             q['realm'] = @trust_root
 792           end
 793         end
 794 
 795         if @assoc_handle
 796           q['assoc_handle'] = @assoc_handle
 797         end
 798 
 799         response = Message.new(@message.get_openid_namespace)
 800         response.update_args(@message.get_openid_namespace, q)
 801         return response.to_url(server_url)
 802       end
 803 
 804       def cancel_url
 805         # Get the URL to cancel this request.
 806         #
 807         # Useful for creating a "Cancel" button on a web form so that
 808         # operation can be carried out directly without another trip
 809         # through the server.
 810         #
 811         # (Except you may want to make another trip through the
 812         # server so that it knows that the user did make a decision.)
 813         #
 814         # Returns a URL as a string.
 815         if !@return_to
 816           raise NoReturnToError
 817         end
 818 
 819         if @immediate
 820           raise ArgumentError.new("Cancel is not an appropriate response to " +
 821                                   "immediate mode requests.")
 822         end
 823 
 824         response = Message.new(@message.get_openid_namespace)
 825         response.set_arg(OPENID_NS, 'mode', 'cancel')
 826         return response.to_url(@return_to)
 827       end
 828 
 829       def to_s
 830         return sprintf('<%s id:%s im:%s tr:%s ah:%s>', self.class,
 831                        @identity,
 832                        @immediate,
 833                        @trust_root,
 834                        @assoc_handle)
 835       end
 836     end
 837 
 838     # I am a response to an OpenID request.
 839     #
 840     # Attributes:
 841     # signed:: A list of the names of the fields which should be signed.
 842     #
 843     # Implementer's note: In a more symmetric client/server
 844     # implementation, there would be more types of #OpenIDResponse
 845     # object and they would have validated attributes according to
 846     # the type of response.  But as it is, Response objects in a
 847     # server are basically write-only, their only job is to go out
 848     # over the wire, so this is just a loose wrapper around
 849     # #OpenIDResponse.fields.
 850     class OpenIDResponse
 851       # The #OpenIDRequest I respond to.
 852       attr_accessor :request
 853 
 854       # An #OpenID::Message with the data to be returned.
 855       # Keys are parameter names with no
 856       # leading openid. e.g. identity and mac_key
 857       # never openid.identity.
 858       attr_accessor :fields
 859 
 860       def initialize(request)
 861         # Make a response to an OpenIDRequest.
 862         @request = request
 863         @fields = Message.new(request.namespace)
 864       end
 865 
 866       def to_s
 867         return sprintf("%s for %s: %s",
 868                        self.class,
 869                        @request.class,
 870                        @fields)
 871       end
 872 
 873       # form_tag_attrs is a hash of attributes to be added to the form
 874       # tag. 'accept-charset' and 'enctype' have defaults that can be
 875       # overridden. If a value is supplied for 'action' or 'method',
 876       # it will be replaced.       
 877       # Returns the form markup for this response.
 878       def to_form_markup(form_tag_attrs=nil)
 879         return @fields.to_form_markup(@request.return_to, form_tag_attrs)
 880       end
 881 
 882       # Wraps the form tag from to_form_markup in a complete HTML document
 883       # that uses javascript to autosubmit the form.
 884       def to_html(form_tag_attrs=nil)
 885         return Util.auto_submit_html(to_form_markup(form_tag_attrs))
 886       end
 887 
 888       def render_as_form
 889         # Returns true if this response's encoding is
 890         # ENCODE_HTML_FORM.  Convenience method for server authors.
 891         return self.which_encoding == ENCODE_HTML_FORM
 892       end
 893 
 894       def needs_signing
 895         # Does this response require signing?
 896         return @fields.get_arg(OPENID_NS, 'mode') == 'id_res'
 897       end
 898 
 899       # implements IEncodable
 900 
 901       def which_encoding
 902         # How should I be encoded?
 903         # returns one of ENCODE_URL or ENCODE_KVFORM.
 904         if BROWSER_REQUEST_MODES.member?(@request.mode)
 905           if @fields.is_openid2 and
 906               encode_to_url.length > OPENID1_URL_LIMIT
 907             return ENCODE_HTML_FORM
 908           else
 909             return ENCODE_URL
 910           end
 911         else
 912           return ENCODE_KVFORM
 913         end
 914       end
 915 
 916       def encode_to_url
 917         # Encode a response as a URL for the user agent to GET.
 918         # You will generally use this URL with a HTTP redirect.
 919         return @fields.to_url(@request.return_to)
 920       end
 921 
 922       def add_extension(extension_response)
 923         # Add an extension response to this response message.
 924         #
 925         # extension_response:: An object that implements the
 926         #     #OpenID::Extension interface for adding arguments to an OpenID
 927         #     message.
 928         extension_response.to_message(@fields)
 929       end
 930 
 931       def encode_to_kvform
 932         # Encode a response in key-value colon/newline format.
 933         #
 934         # This is a machine-readable format used to respond to
 935         # messages which came directly from the consumer and not
 936         # through the user agent.
 937         #
 938         # see: OpenID Specs,
 939         #    <a href="http://openid.net/specs.bml#keyvalue">Key-Value Colon/Newline format</a>
 940         return @fields.to_kvform
 941       end
 942 
 943       def copy
 944         return Marshal.load(Marshal.dump(self))
 945       end
 946     end
 947 
 948     # I am a response to an OpenID request in terms a web server
 949     # understands.
 950     #
 951     # I generally come from an #Encoder, either directly or from
 952     # #Server.encodeResponse.
 953     class WebResponse
 954 
 955       # The HTTP code of this response as an integer.
 956       attr_accessor :code
 957 
 958       # #Hash of headers to include in this response.
 959       attr_accessor :headers
 960 
 961       # The body of this response.
 962       attr_accessor :body
 963 
 964       def initialize(code=HTTP_OK, headers=nil, body="")
 965         # Construct me.
 966         #
 967         # These parameters are assigned directly as class attributes,
 968         # see my class documentation for their
 969         # descriptions.
 970         @code = code
 971         if headers
 972           @headers = headers
 973         else
 974           @headers = {}
 975         end
 976         @body = body
 977       end
 978     end
 979 
 980     # I sign things.
 981     #
 982     # I also check signatures.
 983     #
 984     # All my state is encapsulated in a store, which means I'm not
 985     # generally pickleable but I am easy to reconstruct.
 986     class Signatory
 987       # The number of seconds a secret remains valid. Defaults to 14 days.
 988       attr_accessor :secret_lifetime
 989 
 990       # keys have a bogus server URL in them because the filestore
 991       # really does expect that key to be a URL.  This seems a little
 992       # silly for the server store, since I expect there to be only
 993       # one server URL.
 994       @@_normal_key = 'http://localhost/|normal'
 995       @@_dumb_key = 'http://localhost/|dumb'
 996 
 997       def self._normal_key
 998         @@_normal_key
 999       end
1000 
1001       def self._dumb_key
1002         @@_dumb_key
1003       end
1004 
1005       attr_accessor :store
1006 
1007       # Create a new Signatory. store is The back-end where my
1008       # associations are stored.
1009       def initialize(store)
1010         Util.assert(store)
1011         @store = store
1012         @secret_lifetime = 14 * 24 * 60 * 60
1013       end
1014 
1015       # Verify that the signature for some data is valid.
1016       def verify(assoc_handle, message)
1017         assoc = get_association(assoc_handle, true)
1018         if !assoc
1019           Util.log(sprintf("failed to get assoc with handle %s to verify " +
1020                            "message %s", assoc_handle, message))
1021           return false
1022         end
1023 
1024         begin
1025           valid = assoc.check_message_signature(message)
1026         rescue StandardError => ex
1027           Util.log(sprintf("Error in verifying %s with %s: %s",
1028                            message, assoc, ex))
1029           return false
1030         end
1031 
1032         return valid
1033       end
1034 
1035       # Sign a response.
1036       #
1037       # I take an OpenIDResponse, create a signature for everything in
1038       # its signed list, and return a new copy of the response object
1039       # with that signature included.
1040       def sign(response)
1041         signed_response = response.copy
1042         assoc_handle = response.request.assoc_handle
1043         if assoc_handle
1044           # normal mode disabling expiration check because even if the
1045           # association is expired, we still need to know some
1046           # properties of the association so that we may preserve
1047           # those properties when creating the fallback association.
1048           assoc = get_association(assoc_handle, false, false)
1049 
1050           if !assoc or assoc.expires_in <= 0
1051             # fall back to dumb mode
1052             signed_response.fields.set_arg(
1053                   OPENID_NS, 'invalidate_handle', assoc_handle)
1054             assoc_type = assoc ? assoc.assoc_type : 'HMAC-SHA1'
1055             if assoc and assoc.expires_in <= 0
1056               # now do the clean-up that the disabled checkExpiration
1057               # code didn't get to do.
1058               invalidate(assoc_handle, false)
1059             end
1060             assoc = create_association(true, assoc_type)
1061           end
1062         else
1063           # dumb mode.
1064           assoc = create_association(true)
1065         end
1066 
1067         begin
1068           signed_response.fields = assoc.sign_message(signed_response.fields)
1069         rescue KVFormError => err
1070           raise EncodingError, err
1071         end
1072         return signed_response
1073       end
1074 
1075       # Make a new association.
1076       def create_association(dumb=true, assoc_type='HMAC-SHA1')
1077         secret = CryptUtil.random_string(OpenID.get_secret_size(assoc_type))
1078         uniq = Util.to_base64(CryptUtil.random_string(4))
1079         handle = sprintf('{%s}{%x}{%s}', assoc_type, Time.now.to_i, uniq)
1080 
1081         assoc = Association.from_expires_in(
1082             secret_lifetime, handle, secret, assoc_type)
1083 
1084         if dumb
1085           key = @@_dumb_key
1086         else
1087           key = @@_normal_key
1088         end
1089 
1090         @store.store_association(key, assoc)
1091         return assoc
1092       end
1093 
1094       # Get the association with the specified handle.
1095       def get_association(assoc_handle, dumb, checkExpiration=true)
1096         # Hmm.  We've created an interface that deals almost entirely
1097         # with assoc_handles.  The only place outside the Signatory
1098         # that uses this (and thus the only place that ever sees
1099         # Association objects) is when creating a response to an
1100         # association request, as it must have the association's
1101         # secret.
1102 
1103         if !assoc_handle
1104           raise ArgumentError.new("assoc_handle must not be None")
1105         end
1106 
1107         if dumb
1108           key = @@_dumb_key
1109         else
1110           key = @@_normal_key
1111         end
1112 
1113         assoc = @store.get_association(key, assoc_handle)
1114         if assoc and assoc.expires_in <= 0
1115           Util.log(sprintf("requested %sdumb key %s is expired (by %s seconds)",
1116                            (!dumb) ? 'not-' : '',
1117                            assoc_handle, assoc.expires_in))
1118           if checkExpiration
1119             @store.remove_association(key, assoc_handle)
1120             assoc = nil
1121           end
1122         end
1123 
1124         return assoc
1125       end
1126 
1127       # Invalidates the association with the given handle.
1128       def invalidate(assoc_handle, dumb)
1129         if dumb
1130           key = @@_dumb_key
1131         else
1132           key = @@_normal_key
1133         end
1134 
1135         @store.remove_association(key, assoc_handle)
1136       end
1137     end
1138 
1139     # I encode responses in to WebResponses.
1140     #
1141     # If you don't like WebResponses, you can do
1142     # your own handling of OpenIDResponses with
1143     # OpenIDResponse.whichEncoding,
1144     # OpenIDResponse.encodeToURL, and
1145     # OpenIDResponse.encodeToKVForm.
1146     class Encoder
1147       @@responseFactory = WebResponse
1148 
1149       # Encode a response to a WebResponse.
1150       #
1151       # Raises EncodingError when I can't figure out how to encode
1152       # this message.
1153       def encode(response)
1154         encode_as = response.which_encoding()
1155         if encode_as == ENCODE_KVFORM
1156           wr = @@responseFactory.new(HTTP_OK, nil,
1157                                      response.encode_to_kvform())
1158           if response.is_a?(Exception)
1159             wr.code = HTTP_ERROR
1160           end
1161         elsif encode_as == ENCODE_URL
1162           location = response.encode_to_url()
1163           wr = @@responseFactory.new(HTTP_REDIRECT,
1164                                      {'location' => location})
1165         elsif encode_as == ENCODE_HTML_FORM
1166           wr = @@responseFactory.new(HTTP_OK, nil,
1167                                      response.to_form_markup())
1168         else
1169           # Can't encode this to a protocol message.  You should
1170           # probably render it to HTML and show it to the user.
1171           raise EncodingError.new(response)
1172         end
1173 
1174         return wr
1175       end
1176     end
1177 
1178     # I encode responses in to WebResponses, signing
1179     # them when required.
1180     class SigningEncoder < Encoder
1181 
1182       attr_accessor :signatory
1183 
1184       # Create a SigningEncoder given a Signatory
1185       def initialize(signatory)
1186         @signatory = signatory
1187       end
1188 
1189       # Encode a response to a WebResponse, signing it first if
1190       # appropriate.
1191       #
1192       # Raises EncodingError when I can't figure out how to encode this
1193       # message.
1194       #
1195       # Raises AlreadySigned when this response is already signed.
1196       def encode(response)
1197         # the is_a? is a bit of a kludge... it means there isn't
1198         # really an adapter to make the interfaces quite match.
1199         if !response.is_a?(Exception) and response.needs_signing()
1200           if !@signatory
1201             raise ArgumentError.new(
1202               sprintf("Must have a store to sign this request: %s",
1203                       response), response)
1204           end
1205 
1206           if response.fields.has_key?(OPENID_NS, 'sig')
1207             raise AlreadySigned.new(response)
1208           end
1209 
1210           response = @signatory.sign(response)
1211         end
1212 
1213         return super(response)
1214       end
1215     end
1216 
1217     # I decode an incoming web request in to a OpenIDRequest.
1218     class Decoder
1219 
1220       @@handlers = {
1221         'checkid_setup' => CheckIDRequest.method('from_message'),
1222         'checkid_immediate' => CheckIDRequest.method('from_message'),
1223         'check_authentication' => CheckAuthRequest.method('from_message'),
1224         'associate' => AssociateRequest.method('from_message'),
1225         }
1226 
1227       attr_accessor :server
1228 
1229       # Construct a Decoder. The server is necessary because some
1230       # replies reference their server.
1231       def initialize(server)
1232         @server = server
1233       end
1234 
1235       # I transform query parameters into an OpenIDRequest.
1236       #
1237       # If the query does not seem to be an OpenID request at all, I
1238       # return nil.
1239       #
1240       # Raises ProtocolError when the query does not seem to be a valid
1241       # OpenID request.
1242       def decode(query)
1243         if query.nil? or query.length == 0
1244           return nil
1245         end
1246 
1247         begin
1248           message = Message.from_post_args(query)
1249         rescue InvalidOpenIDNamespace => e
1250           query = query.dup
1251           query['openid.ns'] = OPENID2_NS
1252           message = Message.from_post_args(query)
1253           raise ProtocolError.new(message, e.to_s)
1254         end
1255 
1256         mode = message.get_arg(OPENID_NS, 'mode')
1257         if !mode
1258           msg = sprintf("No mode value in message %s", message)
1259           raise ProtocolError.new(message, msg)
1260         end
1261 
1262         handler = @@handlers.fetch(mode, self.method('default_decoder'))
1263         return handler.call(message, @server.op_endpoint)
1264       end
1265 
1266       # Called to decode queries when no handler for that mode is
1267       # found.
1268       #
1269       # This implementation always raises ProtocolError.
1270       def default_decoder(message, server)
1271         mode = message.get_arg(OPENID_NS, 'mode')
1272         msg = sprintf("Unrecognized OpenID mode %s", mode)
1273         raise ProtocolError.new(message, msg)
1274       end
1275     end
1276 
1277     # I handle requests for an OpenID server.
1278     #
1279     # Some types of requests (those which are not checkid requests)
1280     # may be handed to my handleRequest method, and I will take care
1281     # of it and return a response.
1282     #
1283     # For your convenience, I also provide an interface to
1284     # Decoder.decode and SigningEncoder.encode through my methods
1285     # decodeRequest and encodeResponse.
1286     #
1287     # All my state is encapsulated in an store, which means I'm not
1288     # generally pickleable but I am easy to reconstruct.
1289     class Server
1290       @@signatoryClass = Signatory
1291       @@encoderClass = SigningEncoder
1292       @@decoderClass = Decoder
1293 
1294       # The back-end where my associations and nonces are stored.
1295       attr_accessor :store
1296 
1297       # I'm using this for associate requests and to sign things.
1298       attr_accessor :signatory
1299 
1300       # I'm using this to encode things.
1301       attr_accessor :encoder
1302 
1303       # I'm using this to decode things.
1304       attr_accessor :decoder
1305 
1306       # I use this instance of OpenID::AssociationNegotiator to
1307       # determine which kinds of associations I can make and how.
1308       attr_accessor :negotiator
1309 
1310       # My URL.
1311       attr_accessor :op_endpoint
1312 
1313       # op_endpoint is new in library version 2.0.
1314       def initialize(store, op_endpoint)
1315         @store = store
1316         @signatory = @@signatoryClass.new(@store)
1317         @encoder = @@encoderClass.new(@signatory)
1318         @decoder = @@decoderClass.new(self)