Class: OpenID::Server::CheckIDRequest

Inherits:
OpenIDRequest show all
Defined in:
lib/openid/server.rb

Overview

A request to confirm the identity of a user.

This class handles requests for openid modes checkid_immediate and checkid_setup .

Instance Attribute Summary collapse

Attributes inherited from OpenIDRequest

#message

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from OpenIDRequest

#namespace

Constructor Details

#initialize(identity, return_to, op_endpoint, trust_root = nil, immediate = false, assoc_handle = nil, claimed_id = nil) ⇒ CheckIDRequest

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.



447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
# File 'lib/openid/server.rb', line 447

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

Instance Attribute Details

#assoc_handleObject

Provided in smart mode requests, a handle for a previously established association. nil for dumb mode requests.



415
416
417
# File 'lib/openid/server.rb', line 415

def assoc_handle
  @assoc_handle
end

#claimed_idObject

The claimed identifier. Not present in OpenID 1.x messages.



429
430
431
# File 'lib/openid/server.rb', line 429

def claimed_id
  @claimed_id
end

#identityObject

The OP-local identifier being checked.



425
426
427
# File 'lib/openid/server.rb', line 425

def identity
  @identity
end

#immediateObject

Is this an immediate-mode request?



418
419
420
# File 'lib/openid/server.rb', line 418

def immediate
  @immediate
end

#modeObject

mode

checkid_immediate or checkid_setup



437
438
439
# File 'lib/openid/server.rb', line 437

def mode
  @mode
end

#op_endpointObject

Returns the value of attribute op_endpoint.



439
440
441
# File 'lib/openid/server.rb', line 439

def op_endpoint
  @op_endpoint
end

#return_toObject

The URL to send the user agent back to to reply to this request.



422
423
424
# File 'lib/openid/server.rb', line 422

def return_to
  @return_to
end

#trust_rootObject

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.



434
435
436
# File 'lib/openid/server.rb', line 434

def trust_root
  @trust_root
end

Class Method Details

.from_message(message, op_endpoint) ⇒ Object

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.



490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
# File 'lib/openid/server.rb', line 490

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

Instance Method Details

#answer(allow, server_url = nil, identity = nil, claimed_id = nil) ⇒ Object

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. http://example.com/server), 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.



657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
# File 'lib/openid/server.rb', line 657

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

#cancel_urlObject



804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
# File 'lib/openid/server.rb', line 804

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

#encode_to_url(server_url) ⇒ Object



769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
# File 'lib/openid/server.rb', line 769

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

#id_selectObject

Is the identifier to be selected by the IDP?



573
574
575
576
# File 'lib/openid/server.rb', line 573

def id_select
  # So IDPs don't have to import the constant
  return @identity == IDENTIFIER_SELECT
end

#return_to_verifiedObject

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



612
613
614
# File 'lib/openid/server.rb', line 612

def return_to_verified
  return TrustRoot.verify_return_to(@trust_root, @return_to)
end

#to_sObject



829
830
831
832
833
834
835
# File 'lib/openid/server.rb', line 829

def to_s
  return sprintf('<%s id:%s im:%s tr:%s ah:%s>', self.class,
                 @identity,
                 @immediate,
                 @trust_root,
                 @assoc_handle)
end

#trust_root_validObject

Is my return_to under my trust_root?



579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
# File 'lib/openid/server.rb', line 579

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