saml2 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/lib/saml2.rb +2 -0
  3. data/lib/saml2/assertion.rb +6 -0
  4. data/lib/saml2/attribute.rb +45 -13
  5. data/lib/saml2/attribute/x500.rb +32 -19
  6. data/lib/saml2/attribute_consuming_service.rb +52 -4
  7. data/lib/saml2/authn_request.rb +39 -3
  8. data/lib/saml2/authn_statement.rb +23 -11
  9. data/lib/saml2/base.rb +36 -0
  10. data/lib/saml2/bindings.rb +3 -1
  11. data/lib/saml2/bindings/http_post.rb +17 -1
  12. data/lib/saml2/bindings/http_redirect.rb +54 -9
  13. data/lib/saml2/conditions.rb +43 -16
  14. data/lib/saml2/contact.rb +17 -6
  15. data/lib/saml2/endpoint.rb +13 -0
  16. data/lib/saml2/engine.rb +2 -0
  17. data/lib/saml2/entity.rb +20 -0
  18. data/lib/saml2/identity_provider.rb +11 -1
  19. data/lib/saml2/indexed_object.rb +13 -3
  20. data/lib/saml2/key.rb +89 -32
  21. data/lib/saml2/localized_name.rb +8 -0
  22. data/lib/saml2/logout_request.rb +12 -3
  23. data/lib/saml2/logout_response.rb +9 -0
  24. data/lib/saml2/message.rb +38 -7
  25. data/lib/saml2/name_id.rb +42 -16
  26. data/lib/saml2/namespaces.rb +10 -8
  27. data/lib/saml2/organization.rb +5 -0
  28. data/lib/saml2/organization_and_contacts.rb +5 -0
  29. data/lib/saml2/request.rb +3 -0
  30. data/lib/saml2/requested_authn_context.rb +7 -1
  31. data/lib/saml2/response.rb +20 -2
  32. data/lib/saml2/role.rb +12 -2
  33. data/lib/saml2/schemas.rb +2 -0
  34. data/lib/saml2/service_provider.rb +6 -0
  35. data/lib/saml2/signable.rb +32 -2
  36. data/lib/saml2/sso.rb +7 -0
  37. data/lib/saml2/status.rb +8 -1
  38. data/lib/saml2/status_response.rb +7 -1
  39. data/lib/saml2/subject.rb +22 -5
  40. data/lib/saml2/version.rb +3 -1
  41. data/spec/lib/bindings/http_redirect_spec.rb +23 -2
  42. data/spec/lib/conditions_spec.rb +10 -11
  43. data/spec/lib/identity_provider_spec.rb +1 -1
  44. data/spec/lib/service_provider_spec.rb +7 -2
  45. metadata +5 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c776c54f1dc7f9b26b39544abd6bdde6c2f68a1f28339627ad9d8745cdfc4ee7
4
- data.tar.gz: 703170dd49abeb43b09070b6000f748737d1ccbe0374b154c274f84065935daa
3
+ metadata.gz: 7882f9e6593924c18faed398409e973ca227162a990557156d7992a51c4f7cdb
4
+ data.tar.gz: c862c3fd1c560068e59340ba4f19a3d810807b6c22a4c36c07fb0ef9a6711132
5
5
  SHA512:
6
- metadata.gz: 74fe75be6913966fd33a9e72535398a2ecc6916f483aeb47b3202afba09fb175b3c2234c8dfb63a0d48b1aa9f3536204635471e0ab73c125b8276d063838e7cb
7
- data.tar.gz: dd4f61cc065ebfc647c4306a084b56e5eb5fd57877898f05a67a09c5df87837f34d8c503ca933aa2f50cc770f44e6712bc0ee9192a0f9d07a48d8fb4a974154f
6
+ metadata.gz: 89aa07641f0625f8f9fd781cb41a169349567531984c1fe2adc278ae0568687c4e4822be1a9c00e0c561116ffbc39d140abde34a1be439bd36b1a3869d34dcf5
7
+ data.tar.gz: f07d6fbf9dc816d3223ae9cd1728b3232b469b61057ae7e6e14518efd939e3e8e4aff5c1e3b4a3c56584d7e5dbc9abc33dab180452a8b5d9b5ff7e34012c15e3
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'saml2/authn_request'
2
4
  require 'saml2/entity'
3
5
  require 'saml2/logout_request'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'saml2/conditions'
2
4
 
3
5
  module SAML2
@@ -16,6 +18,7 @@ module SAML2
16
18
  @statements = nil
17
19
  end
18
20
 
21
+ # @return [Subject, nil]
19
22
  def subject
20
23
  if xml && !instance_variable_defined?(:@subject)
21
24
  @subject = Subject.from_xml(xml.at_xpath('saml:Subject', Namespaces::ALL))
@@ -23,14 +26,17 @@ module SAML2
23
26
  @subject
24
27
  end
25
28
 
29
+ # @return [Conditions]
26
30
  def conditions
27
31
  @conditions ||= Conditions.from_xml(xml.at_xpath('saml:Conditions', Namespaces::ALL))
28
32
  end
29
33
 
34
+ # @return [Array<AuthnStatement, AttributeStatement>]
30
35
  def statements
31
36
  @statements ||= load_object_array(xml, 'saml:AuthnStatement|saml:AttributeStatement')
32
37
  end
33
38
 
39
+ # (see Base#build)
34
40
  def build(builder)
35
41
  builder['saml'].Assertion(
36
42
  'xmlns:saml' => Namespaces::SAML
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
2
4
 
3
5
  require 'active_support/core_ext/array/wrap'
@@ -8,20 +10,13 @@ require 'saml2/namespaces'
8
10
  module SAML2
9
11
  class Attribute < Base
10
12
  module NameFormats
11
- BASIC = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic".freeze
12
- UNSPECIFIED = "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified".freeze
13
- URI = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri".freeze
13
+ BASIC = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
14
+ UNSPECIFIED = "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"
15
+ URI = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
14
16
  end
15
17
 
16
18
  class << self
17
- def subclasses
18
- @subclasses ||= []
19
- end
20
-
21
- def inherited(klass)
22
- subclasses << klass
23
- end
24
-
19
+ # (see Base.from_xml)
25
20
  def from_xml(node)
26
21
  # pass through for subclasses
27
22
  return super unless self == Attribute
@@ -31,20 +26,43 @@ module SAML2
31
26
  klass ? klass.from_xml(node) : super
32
27
  end
33
28
 
29
+ # Create an appropriate object to represent an attribute.
30
+ #
31
+ # This will create the most appropriate object (i.e. an
32
+ # {Attribute::X500} if possible) to represent this attribute,
33
+ # based on its name.
34
+ # @param name [String]
35
+ # The attribute name. This can be a friendly name, or a URI.
36
+ # @param value optional
37
+ # The attribute value.
38
+ # @return [Attribute]
34
39
  def create(name, value = nil)
35
40
  (class_for(name) || self).new(name, value)
36
41
  end
37
42
 
43
+ # The XML namespace that this attribute class serializes as.
44
+ # @return ['saml']
38
45
  def namespace
39
46
  'saml'
40
47
  end
41
48
 
49
+ # The XML element that this attribute class serializes as.
50
+ # @return ['Attribute']
42
51
  def element
43
52
  'Attribute'
44
53
  end
45
54
 
46
55
  protected
47
56
 
57
+ def subclasses
58
+ @subclasses ||= []
59
+ end
60
+
61
+ def inherited(klass)
62
+ subclasses << klass
63
+ end
64
+
65
+
48
66
  def class_for(name_or_node)
49
67
  subclasses.find do |klass|
50
68
  klass.respond_to?(:recognizes?) && klass.recognizes?(name_or_node)
@@ -52,12 +70,24 @@ module SAML2
52
70
  end
53
71
  end
54
72
 
55
- attr_accessor :name, :friendly_name, :name_format, :value
56
-
73
+ # @return [String]
74
+ attr_accessor :name
75
+ # @return [String, nil]
76
+ attr_accessor :friendly_name, :name_format
77
+ # @return [Object, nil]
78
+ attr_accessor :value
79
+
80
+ # Create a new generic Attribute
81
+ #
82
+ # @param name [String]
83
+ # @param value optional [Object, nil]
84
+ # @param friendly_name optional [String, nil]
85
+ # @param name_format optional [String, nil]
57
86
  def initialize(name = nil, value = nil, friendly_name = nil, name_format = nil)
58
87
  @name, @value, @friendly_name, @name_format = name, value, friendly_name, name_format
59
88
  end
60
89
 
90
+ # (see Base#build)
61
91
  def build(builder)
62
92
  builder[self.class.namespace].__send__(self.class.element, 'Name' => name) do |attribute|
63
93
  attribute.parent['FriendlyName'] = friendly_name if friendly_name
@@ -71,6 +101,7 @@ module SAML2
71
101
  end
72
102
  end
73
103
 
104
+ # (see Base#from_xml)
74
105
  def from_xml(node)
75
106
  super
76
107
  @name = node['Name']
@@ -87,6 +118,7 @@ module SAML2
87
118
  end
88
119
 
89
120
  private
121
+
90
122
  XS_TYPES = {
91
123
  lookup_qname('xs:boolean', Namespaces::ALL) =>
92
124
  [[TrueClass, FalseClass], nil, ->(v) { %w{true 1}.include?(v) ? true : false }],
@@ -1,34 +1,38 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SAML2
2
4
  class Attribute
3
5
  class X500 < Attribute
4
- GIVEN_NAME = 'urn:oid:2.5.4.42'.freeze
5
- SN = SURNAME = 'urn:oid:2.5.4.4'.freeze
6
+ GIVEN_NAME = 'urn:oid:2.5.4.42'
7
+ SN = SURNAME = 'urn:oid:2.5.4.4'
6
8
  # https://www.ietf.org/rfc/rfc2798.txt
7
9
  module InetOrgPerson
8
- DISPLAY_NAME = 'urn:oid:2.16.840.1.113730.3.1.241'.freeze
9
- EMPLOYEE_NUMBER = 'urn:oid:2.16.840.1.113730.3.1.3'.freeze
10
- EMPLOYEE_TYPE = 'urn:oid:2.16.840.1.113730.3.1.4'.freeze
11
- PREFERRED_LANGUAGE = 'urn:oid:2.16.840.1.113730.3.1.39'.freeze
10
+ DISPLAY_NAME = 'urn:oid:2.16.840.1.113730.3.1.241'
11
+ EMPLOYEE_NUMBER = 'urn:oid:2.16.840.1.113730.3.1.3'
12
+ EMPLOYEE_TYPE = 'urn:oid:2.16.840.1.113730.3.1.4'
13
+ PREFERRED_LANGUAGE = 'urn:oid:2.16.840.1.113730.3.1.39'
12
14
  end
13
15
  # https://www.internet2.edu/media/medialibrary/2013/09/04/internet2-mace-dir-eduperson-201203.html
14
16
  module EduPerson
15
- AFFILIATION = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.1'.freeze
16
- ASSURANCE = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.11'.freeze
17
- ENTITLEMENT = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7'.freeze
18
- NICKNAME = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.2'.freeze
19
- ORG_D_N = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.3'.freeze
20
- PRIMARY_AFFILIATION = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.5'.freeze
21
- PRIMARY_ORG_UNIT_D_N = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.8'.freeze
22
- PRINCIPAL_NAME = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6'.freeze
23
- SCOPED_AFFILIATION = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.9'.freeze
24
- TARGETED_I_D = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.10'.freeze
25
- UNIT_D_N = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.4'.freeze
17
+ AFFILIATION = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.1'
18
+ ASSURANCE = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.11'
19
+ ENTITLEMENT = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7'
20
+ NICKNAME = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.2'
21
+ ORG_D_N = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.3'
22
+ PRIMARY_AFFILIATION = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.5'
23
+ PRIMARY_ORG_UNIT_D_N = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.8'
24
+ PRINCIPAL_NAME = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6'
25
+ SCOPED_AFFILIATION = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.9'
26
+ TARGETED_I_D = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.10'
27
+ UNIT_D_N = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.4'
26
28
  end
27
29
  # http://www.ietf.org/rfc/rfc4519.txt
28
- UID = USERID = 'urn:oid:0.9.2342.19200300.100.1.1'.freeze
30
+ UID = USERID = 'urn:oid:0.9.2342.19200300.100.1.1'
29
31
  # http://www.ietf.org/rfc/rfc4524.txt
30
- MAIL = 'urn:oid:0.9.2342.19200300.100.1.3'.freeze
32
+ MAIL = 'urn:oid:0.9.2342.19200300.100.1.3'
31
33
 
34
+ # Returns true if the param should be an {X500} Attribute.
35
+ # @param name_or_node [String, Nokogiri::XML::Element]
32
36
  def self.recognizes?(name_or_node)
33
37
  if name_or_node.is_a?(Nokogiri::XML::Element)
34
38
  !!name_or_node.at_xpath("@x500:Encoding", Namespaces::ALL)
@@ -37,6 +41,14 @@ module SAML2
37
41
  end
38
42
  end
39
43
 
44
+ # Create a new X.500 attribute.
45
+ #
46
+ # The name format will always be set to URI.
47
+ #
48
+ # @param name [String]
49
+ # Either an OID or a known friendly name. The opposite value will be
50
+ # inferred automatically.
51
+ # @param value optional [Object, nil]
40
52
  def initialize(name = nil, value = nil)
41
53
  # if they pass an OID, infer the friendly name
42
54
  friendly_name = OIDS[name]
@@ -51,6 +63,7 @@ module SAML2
51
63
  super(name, value, friendly_name, NameFormats::URI)
52
64
  end
53
65
 
66
+ # (see Base#build)
54
67
  def build(builder)
55
68
  super
56
69
  attr = builder.parent.last_element_child
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/core_ext/array/wrap'
2
4
 
3
5
  require 'saml2/attribute'
@@ -8,40 +10,61 @@ require 'saml2/namespaces'
8
10
  module SAML2
9
11
  class RequestedAttribute < Attribute
10
12
  class << self
13
+ # The XML namespace that this attribute class serializes as.
14
+ # @return ['md']
11
15
  def namespace
12
16
  'md'
13
17
  end
14
18
 
19
+ # The XML element that this attribute class serializes as.
20
+ # @return ['RequestedAttribute']
15
21
  def element
16
22
  'RequestedAttribute'
17
23
  end
18
24
 
25
+ # Create a RequestAttribute object to represent an attribute.
26
+ #
27
+ # {Attribute.create} will be used to create a temporary object, so that
28
+ # attribute-class specific inferences (i.e. {Attribute::X500} friendly
29
+ # names) will be done, but always returns a {RequestedAttribute}.
30
+ # @param name [String]
31
+ # The attribute name. This can be a friendly name, or a URI.
32
+ # @param is_required optional [true, false, nil]
33
+ # @return [RequestedAttribute]
19
34
  def create(name, is_required = nil)
20
- # use Attribute.create to get other subclasses to automatically fill out friendly_name
21
- # and name_format, but still return a RequestedAttribute object
22
35
  attribute = Attribute.create(name)
23
36
  new(attribute.name, is_required, attribute.friendly_name, attribute.name_format)
24
37
  end
25
38
  end
26
39
 
40
+ # Create a new {RequestedAttribute}.
41
+ #
42
+ # @param name [String]
43
+ # @param is_required optional [true, false, nil]
44
+ # @param friendly_name optional [String, nil]
45
+ # @param name_format optional [String, nil]
27
46
  def initialize(name = nil, is_required = nil, friendly_name = nil, name_format = nil)
28
47
  super(name, nil, friendly_name, name_format)
29
48
  @is_required = is_required
30
49
  end
31
50
 
51
+ # (see Base#from_xml)
32
52
  def from_xml(node)
33
53
  super
34
54
  @is_required = node['isRequired'] && node['isRequired'] == 'true'
35
55
  end
36
56
 
57
+ # @return [true, false, nil]
37
58
  def required?
38
59
  @is_required
39
60
  end
40
61
  end
41
62
 
42
63
  class RequiredAttributeMissing < RuntimeError
64
+ # @return [RequestedAttribute]
43
65
  attr_reader :requested_attribute
44
66
 
67
+ # @param requested_attribute [RequestedAttribute]
45
68
  def initialize(requested_attribute)
46
69
  super("Required attribute #{requested_attribute.name} not provided")
47
70
  @requested_attribute = requested_attribute
@@ -49,8 +72,11 @@ module SAML2
49
72
  end
50
73
 
51
74
  class InvalidAttributeValue < RuntimeError
52
- attr_reader :requested_attribute, :provided_value
75
+ # @return [RequestedAttribute]
76
+ attr_reader :requested_attribute
77
+ attr_reader :provided_value
53
78
 
79
+ # @param requested_attribute [RequestedAttribute]
54
80
  def initialize(requested_attribute, provided_value)
55
81
  super("Attribute #{requested_attribute.name} is provided value " \
56
82
  "#{provided_value.inspect}, but only allows " \
@@ -62,8 +88,13 @@ module SAML2
62
88
  class AttributeConsumingService < Base
63
89
  include IndexedObject
64
90
 
65
- attr_reader :name, :description, :requested_attributes
91
+ # @return [LocalizedName]
92
+ attr_reader :name, :description
93
+ # @return [Array<RequestedAttribute>]
94
+ attr_reader :requested_attributes
66
95
 
96
+ # @param name [String]
97
+ # @param requested_attributes [::Array<RequestedAttributes>]
67
98
  def initialize(name = nil, requested_attributes = [])
68
99
  super()
69
100
  @name = LocalizedName.new('ServiceName', name)
@@ -71,6 +102,7 @@ module SAML2
71
102
  @requested_attributes = requested_attributes
72
103
  end
73
104
 
105
+ # (see Base#from_xml)
74
106
  def from_xml(node)
75
107
  super
76
108
  name.from_xml(node.xpath('md:ServiceName', Namespaces::ALL))
@@ -78,6 +110,21 @@ module SAML2
78
110
  @requested_attributes = load_object_array(node, "md:RequestedAttribute", RequestedAttribute)
79
111
  end
80
112
 
113
+ # Create an {AttributeStatement} from the given attributes hash.
114
+ #
115
+ # Given a set of attributes, create and return an {AttributeStatement}
116
+ # with only the attributes that this {AttributeConsumingService} requests.
117
+ #
118
+ # @param attributes [Hash<String => Object>, Array<Attribute>]
119
+ # If it's a hash, the elements are run through {Attribute.create} first
120
+ # in order to create proper {Attribute} objects.
121
+ # @return [AttributeStatement]
122
+ # @raise [InvalidAttributeValue]
123
+ # If a {RequestedAttribute} specifies that only specific values are
124
+ # permissible, and the provided attribute does not match that value.
125
+ # @raise [RequiredAttributeMissing]
126
+ # If a {RequestedAttribute} is tagged as required, but it has not been
127
+ # supplied.
81
128
  def create_statement(attributes)
82
129
  if attributes.is_a?(Hash)
83
130
  attributes = attributes.map { |k, v| Attribute.create(k, v) }
@@ -119,6 +166,7 @@ module SAML2
119
166
  AttributeStatement.new(attributes)
120
167
  end
121
168
 
169
+ # (see Base#build)
122
170
  def build(builder)
123
171
  builder['md'].AttributeConsumingService do |attribute_consuming_service|
124
172
  name.build(attribute_consuming_service)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'base64'
2
4
  require 'zlib'
3
5
 
@@ -16,12 +18,22 @@ module SAML2
16
18
  attr_writer :assertion_consumer_service_index,
17
19
  :assertion_consumer_service_url,
18
20
  :attribute_consuming_service_index,
19
- :force_authn,
20
21
  :name_id_policy,
21
- :passive,
22
22
  :protocol_binding
23
+ # @return [Boolean, nil]
24
+ attr_writer :force_authn, :passive
25
+ # @return [RequestedAuthnContext, nil]
23
26
  attr_accessor :requested_authn_context
24
27
 
28
+ # Initiate a SAML SSO flow, from a service provider to an identity
29
+ # provider.
30
+ # @todo go over these params, and use kwargs. Maybe pass Entity instead
31
+ # of ServiceProvider.
32
+ # @param issuer [NameID]
33
+ # @param identity_provider [IdentityProvider]
34
+ # @param assertion_consumer_service [Endpoint::Indexed]
35
+ # @param service_provider [ServiceProvider]
36
+ # @return [AuthnRequest]
25
37
  def self.initiate(issuer, identity_provider = nil,
26
38
  assertion_consumer_service: nil,
27
39
  service_provider: nil)
@@ -37,6 +49,7 @@ module SAML2
37
49
  authn_request
38
50
  end
39
51
 
52
+ # @see https://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf section 4.1
40
53
  def valid_web_browser_sso_profile?
41
54
  return false unless issuer
42
55
  return false if issuer.format && issuer.format != NameID::Format::ENTITY
@@ -44,6 +57,7 @@ module SAML2
44
57
  true
45
58
  end
46
59
 
60
+ # @see https://saml2int.org/profile/current/#section82
47
61
  def valid_interoperable_profile?
48
62
  # It's a subset of Web Browser SSO profile
49
63
  return false unless valid_web_browser_sso_profile?
@@ -55,6 +69,14 @@ module SAML2
55
69
  true
56
70
  end
57
71
 
72
+ # Populate {#assertion_consumer_service} and {#attribute_consuming_service}
73
+ # attributes.
74
+ #
75
+ # Given {ServiceProvider} metadata, resolve the index/urls in this object to actual
76
+ # objects.
77
+ #
78
+ # @param service_provider [ServiceProvider]
79
+ # @return [Boolean]
58
80
  def resolve(service_provider)
59
81
  # TODO: check signature if present
60
82
 
@@ -71,6 +93,7 @@ module SAML2
71
93
  true
72
94
  end
73
95
 
96
+ # @return [NameID::Policy, nil]
74
97
  def name_id_policy
75
98
  if xml && !instance_variable_defined?(:@name_id_policy)
76
99
  @name_id_policy = NameID::Policy.from_xml(xml.at_xpath('samlp:NameIDPolicy', Namespaces::ALL))
@@ -78,8 +101,14 @@ module SAML2
78
101
  @name_id_policy
79
102
  end
80
103
 
81
- attr_reader :assertion_consumer_service, :attribute_consuming_service
104
+ # Must call {#resolve} before accessing.
105
+ # @return [AssertionConsumerService, nil]
106
+ attr_reader :assertion_consumer_service
107
+ # Must call {#resolve} before accessing.
108
+ # @return [AttributeConsumingService, nil]
109
+ attr_reader :attribute_consuming_service
82
110
 
111
+ # @return [Integer, nil]
83
112
  def assertion_consumer_service_index
84
113
  if xml && !instance_variable_defined?(:@assertion_consumer_service_index)
85
114
  @assertion_consumer_service_index = xml['AssertionConsumerServiceIndex']&.to_i
@@ -87,6 +116,7 @@ module SAML2
87
116
  @assertion_consumer_service_index
88
117
  end
89
118
 
119
+ # @return [String, nil]
90
120
  def assertion_consumer_service_url
91
121
  if xml && !instance_variable_defined?(:@assertion_consumer_service_url)
92
122
  @assertion_consumer_service_url = xml['AssertionConsumerServiceURL']
@@ -94,6 +124,7 @@ module SAML2
94
124
  @assertion_consumer_service_url
95
125
  end
96
126
 
127
+ # @return [Integer, nil]
97
128
  def attribute_consuming_service_index
98
129
  if xml && !instance_variable_defined?(:@attribute_consuming_service_index)
99
130
  @attribute_consuming_service_index = xml['AttributeConsumingServiceIndex']&.to_i
@@ -101,6 +132,7 @@ module SAML2
101
132
  @attribute_consuming_service_index
102
133
  end
103
134
 
135
+ # @return [true, false, nil]
104
136
  def force_authn?
105
137
  if xml && !instance_variable_defined?(:@force_authn)
106
138
  @force_authn = xml['ForceAuthn']&.== 'true'
@@ -108,6 +140,7 @@ module SAML2
108
140
  @force_authn
109
141
  end
110
142
 
143
+ # @return [true, false, nil]
111
144
  def passive?
112
145
  if xml && !instance_variable_defined?(:@passive)
113
146
  @passive = xml['IsPassive']&.== 'true'
@@ -115,6 +148,7 @@ module SAML2
115
148
  @passive
116
149
  end
117
150
 
151
+ # @return [String, nil]
118
152
  def protocol_binding
119
153
  if xml && !instance_variable_defined?(:@protocol_binding)
120
154
  @protocol_binding = xml['ProtocolBinding']
@@ -122,6 +156,7 @@ module SAML2
122
156
  @protocol_binding
123
157
  end
124
158
 
159
+ # @return [Subject, nil]
125
160
  def subject
126
161
  if xml && !instance_variable_defined?(:@subject)
127
162
  @subject = Subject.from_xml(xml.at_xpath('saml:Subject', Namespaces::ALL))
@@ -129,6 +164,7 @@ module SAML2
129
164
  @subject
130
165
  end
131
166
 
167
+ # (see Base#build)
132
168
  def build(builder)
133
169
  builder['samlp'].AuthnRequest(
134
170
  'xmlns:samlp' => Namespaces::SAMLP,