saml2 1.0.10 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +2 -7
  3. data/lib/saml2.rb +2 -0
  4. data/lib/saml2/attribute.rb +2 -0
  5. data/lib/saml2/attribute_consuming_service.rb +1 -0
  6. data/lib/saml2/authn_request.rb +19 -47
  7. data/lib/saml2/base.rb +5 -2
  8. data/lib/saml2/bindings.rb +7 -0
  9. data/lib/saml2/bindings/http_redirect.rb +141 -0
  10. data/lib/saml2/contact.rb +14 -16
  11. data/lib/saml2/endpoint.rb +5 -6
  12. data/lib/saml2/entity.rb +23 -18
  13. data/lib/saml2/identity_provider.rb +4 -4
  14. data/lib/saml2/indexed_object.rb +7 -3
  15. data/lib/saml2/key.rb +19 -1
  16. data/lib/saml2/logout_request.rb +43 -0
  17. data/lib/saml2/logout_response.rb +23 -0
  18. data/lib/saml2/message.rb +109 -0
  19. data/lib/saml2/name_id.rb +16 -8
  20. data/lib/saml2/organization_and_contacts.rb +2 -2
  21. data/lib/saml2/request.rb +8 -0
  22. data/lib/saml2/response.rb +7 -23
  23. data/lib/saml2/role.rb +2 -3
  24. data/lib/saml2/service_provider.rb +24 -2
  25. data/lib/saml2/sso.rb +2 -2
  26. data/lib/saml2/status.rb +28 -0
  27. data/lib/saml2/status_response.rb +33 -0
  28. data/lib/saml2/version.rb +1 -1
  29. data/spec/fixtures/identity_provider.xml +1 -0
  30. data/spec/fixtures/response_signed.xml +1 -1
  31. data/spec/fixtures/response_with_attribute_signed.xml +1 -1
  32. data/spec/lib/attribute_consuming_service_spec.rb +37 -37
  33. data/spec/lib/attribute_spec.rb +17 -17
  34. data/spec/lib/authn_request_spec.rb +15 -71
  35. data/spec/lib/bindings/http_redirect_spec.rb +151 -0
  36. data/spec/lib/conditions_spec.rb +10 -10
  37. data/spec/lib/entity_spec.rb +12 -12
  38. data/spec/lib/identity_provider_spec.rb +4 -4
  39. data/spec/lib/indexed_object_spec.rb +38 -7
  40. data/spec/lib/logout_request_spec.rb +31 -0
  41. data/spec/lib/logout_response_spec.rb +31 -0
  42. data/spec/lib/message_spec.rb +21 -0
  43. data/spec/lib/response_spec.rb +8 -9
  44. data/spec/lib/service_provider_spec.rb +29 -8
  45. data/spec/spec_helper.rb +0 -1
  46. metadata +41 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7c19f162d8dcc43d0a264bc4c1b1f1f0e497b476
4
- data.tar.gz: e0d34ccb811ce49f36449d7ec843f2f58516a733
3
+ metadata.gz: 382418cbc200bcf81a58ff09ebadd551d0c16101
4
+ data.tar.gz: 0f7318a0d65eb9b80afbe0405d6dc768cc4e3f0d
5
5
  SHA512:
6
- metadata.gz: 70083ff8f78e8eaf3302c548babff43b54478d7bc5dc45346612e18b378695570dbcae619c1776150d80bee59f60dcf658b7848fd78fe0e9e3b592f7e6c7468a
7
- data.tar.gz: 863e91a34172a34526f20935b092160185e2189edf3881d98e7cb82d577c89d16ff1d5a4c21ad0b08eee602348523b74326409161b5d0aa59e84191f6baaef1d
6
+ metadata.gz: 0b0462631aa2d2c0208655bf85e8a104555034176df8825f42029f1356a745d07f561f15a5c78719e8a1400eb70063cc89c6014abaf226a3294d5a6a0aa319c1
7
+ data.tar.gz: c1ad828d647e905e3e92e4e34c2e4856a370fe32abf9bfe337e344dd34e46a95b9d344f5c3cd7542a1636a068febad623af0a83ee2cc524d7c0e74eb7cb8a61e
data/Rakefile CHANGED
@@ -2,12 +2,7 @@ require 'rubygems'
2
2
  require 'bundler'
3
3
  Bundler::GemHelper.install_tasks
4
4
 
5
- require 'rake'
6
- require 'rake/testtask'
7
-
8
- Rake::TestTask.new do |t|
9
- t.name = "spec"
10
- t.pattern = "spec/**/*_spec.rb"
11
- end
5
+ require 'rspec/core/rake_task'
6
+ RSpec::Core::RakeTask.new
12
7
 
13
8
  task :default => :spec
@@ -1,5 +1,7 @@
1
1
  require 'saml2/authn_request'
2
2
  require 'saml2/entity'
3
+ require 'saml2/logout_request'
4
+ require 'saml2/logout_response'
3
5
  require 'saml2/response'
4
6
  require 'saml2/version'
5
7
 
@@ -65,6 +65,7 @@ module SAML2
65
65
  end
66
66
 
67
67
  def from_xml(node)
68
+ super
68
69
  @name = node['Name']
69
70
  @friendly_name = node['FriendlyName']
70
71
  @name_format = node['NameFormat']
@@ -124,6 +125,7 @@ module SAML2
124
125
  end
125
126
 
126
127
  def from_xml(node)
128
+ super
127
129
  @attributes = node.xpath('saml:Attribute', Namespaces::ALL).map do |attr|
128
130
  Attribute.from_xml(attr)
129
131
  end
@@ -52,6 +52,7 @@ module SAML2
52
52
  end
53
53
 
54
54
  def from_xml(node)
55
+ super
55
56
  @name = node['ServiceName']
56
57
  @requested_attributes = load_object_array(node, "md:RequestedAttribute", RequestedAttribute)
57
58
  end
@@ -2,49 +2,29 @@ require 'base64'
2
2
  require 'zlib'
3
3
 
4
4
  require 'saml2/attribute_consuming_service'
5
+ require 'saml2/bindings/http_redirect'
5
6
  require 'saml2/endpoint'
6
7
  require 'saml2/name_id'
7
8
  require 'saml2/namespaces'
9
+ require 'saml2/request'
8
10
  require 'saml2/schemas'
9
11
  require 'saml2/subject'
10
12
 
11
13
  module SAML2
12
- class MessageTooLarge < RuntimeError
13
- end
14
-
15
- class AuthnRequest
14
+ class AuthnRequest < Request
15
+ # deprecated; takes _just_ the SAMLRequest parameter's value
16
16
  def self.decode(authnrequest)
17
- begin
18
- raise MessageTooLarge if authnrequest.bytesize > SAML2.config[:max_message_size]
19
- authnrequest = Base64.decode64(authnrequest)
20
- zstream = Zlib::Inflate.new(-Zlib::MAX_WBITS)
21
- xml = ''
22
- # do it in 1K slices, so we can protect against bombs
23
- (0..authnrequest.bytesize / 1024).each do |i|
24
- xml.concat(zstream.inflate(authnrequest.byteslice(i * 1024, 1024)))
25
- raise MessageTooLarge if xml.bytesize > SAML2.config[:max_message_size]
26
- end
27
- xml.concat(zstream.finish)
28
- raise MessageTooLarge if xml.bytesize > SAML2.config[:max_message_size]
29
-
30
- zstream.close
31
- rescue Zlib::DataError, Zlib::BufError
32
- end
33
- parse(xml)
34
- end
35
-
36
- def self.parse(authnrequest)
37
- new(Nokogiri::XML(authnrequest))
38
- end
39
-
40
- def initialize(document)
41
- @document = document
17
+ result, _relay_state = Bindings::HTTPRedirect.decode("http://host/?SAMLRequest=#{authnrequest}")
18
+ return nil unless result.is_a?(AuthnRequest)
19
+ result
20
+ rescue CorruptMessage
21
+ AuthnRequest.from_xml(Nokogiri::XML('<xml></xml>').root)
42
22
  end
43
23
 
44
24
  def valid_schema?
45
- return false unless Schemas.protocol.valid?(@document)
25
+ return false unless super
46
26
  # Check for the correct root element
47
- return false unless @document.at_xpath('/samlp:AuthnRequest', Namespaces::ALL)
27
+ return false unless xml.at_xpath('/samlp:AuthnRequest', Namespaces::ALL)
48
28
 
49
29
  true
50
30
  end
@@ -83,46 +63,38 @@ module SAML2
83
63
  true
84
64
  end
85
65
 
86
- def issuer
87
- @issuer ||= NameID.from_xml(@document.root.at_xpath('saml:Issuer', Namespaces::ALL))
88
- end
89
-
90
66
  def name_id_policy
91
- @name_id_policy ||= NameID::Policy.from_xml(@document.root.at_xpath('samlp:NameIDPolicy', Namespaces::ALL))
92
- end
93
-
94
- def id
95
- @document.root['ID']
67
+ @name_id_policy ||= NameID::Policy.from_xml(xml.at_xpath('samlp:NameIDPolicy', Namespaces::ALL))
96
68
  end
97
69
 
98
70
  attr_reader :assertion_consumer_service, :attribute_consuming_service
99
71
 
100
72
  def assertion_consumer_service_url
101
- @document.root['AssertionConsumerServiceURL']
73
+ xml['AssertionConsumerServiceURL']
102
74
  end
103
75
 
104
76
  def assertion_consumer_service_index
105
- @document.root['AssertionConsumerServiceIndex'] && @document.root['AssertionConsumerServiceIndex'].to_i
77
+ xml['AssertionConsumerServiceIndex'] && xml['AssertionConsumerServiceIndex'].to_i
106
78
  end
107
79
 
108
80
  def attribute_consuming_service_index
109
- @document.root['AttributeConsumerServiceIndex'] && @document.root['AttributeConsumerServiceIndex'].to_i
81
+ xml['AttributeConsumerServiceIndex'] && xml['AttributeConsumerServiceIndex'].to_i
110
82
  end
111
83
 
112
84
  def force_authn?
113
- @document.root['ForceAuthn']
85
+ xml['ForceAuthn']
114
86
  end
115
87
 
116
88
  def passive?
117
- @document.root['IsPassive']
89
+ xml['IsPassive']
118
90
  end
119
91
 
120
92
  def protocol_binding
121
- @document.root['ProtocolBinding']
93
+ xml['ProtocolBinding']
122
94
  end
123
95
 
124
96
  def subject
125
- @subject ||= Subject.from_xml(@document.at_xpath('saml:Subject', Namespaces::ALL))
97
+ @subject ||= Subject.from_xml(xml.at_xpath('saml:Subject', Namespaces::ALL))
126
98
  end
127
99
  end
128
100
  end
@@ -9,7 +9,10 @@ module SAML2
9
9
  result
10
10
  end
11
11
 
12
- def from_xml(_node)
12
+ attr_reader :xml
13
+
14
+ def from_xml(node)
15
+ @xml = node
13
16
  end
14
17
 
15
18
  def to_s
@@ -28,7 +31,7 @@ module SAML2
28
31
 
29
32
  def self.load_string_array(node, element)
30
33
  node.xpath(element, Namespaces::ALL).map do |element_node|
31
- element_node.content && element_node.content.strip
34
+ element_node.content&.strip
32
35
  end
33
36
  end
34
37
 
@@ -0,0 +1,7 @@
1
+ module SAML2
2
+ module Bindings
3
+ module Encodings
4
+ DEFLATE = 'urn:oasis:names:tc:SAML:2.0:bindings:URL-Encoding:DEFLATE'.freeze
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,141 @@
1
+ require 'base64'
2
+ require 'uri'
3
+ require 'zlib'
4
+
5
+ require 'saml2/bindings'
6
+ require 'saml2/message'
7
+
8
+ module SAML2
9
+ module Bindings
10
+ module HTTPRedirect
11
+ URN ="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect".freeze
12
+
13
+ module SigAlgs
14
+ DSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#dsa-sha1".freeze
15
+ RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1".freeze
16
+
17
+ RECOGNIZED = [DSA_SHA1, RSA_SHA1].freeze
18
+ end
19
+
20
+ class << self
21
+ def decode(url, public_key: nil, public_key_used: nil)
22
+ uri = begin
23
+ URI.parse(url)
24
+ rescue URI::InvalidURIError
25
+ raise CorruptMessage
26
+ end
27
+
28
+ raise MissingMessage unless uri.query
29
+ query = URI.decode_www_form(uri.query)
30
+ base64 = query.assoc('SAMLRequest')&.last
31
+ if base64
32
+ message_param = 'SAMLRequest'
33
+ else
34
+ base64 = query.assoc('SAMLResponse')&.last
35
+ message_param = 'SAMLResponse'
36
+ end
37
+ encoding = query.assoc('SAMLEncoding')&.last
38
+ relay_state = query.assoc('RelayState')&.last
39
+ signature = query.assoc('Signature')&.last
40
+ sig_alg = query.assoc('SigAlg')&.last
41
+ raise MissingMessage unless base64
42
+
43
+ raise UnsupportedEncoding if encoding && encoding != Encodings::DEFLATE
44
+
45
+ raise MessageTooLarge if base64.bytesize > SAML2.config[:max_message_size]
46
+
47
+ deflated = begin
48
+ Base64.strict_decode64(base64)
49
+ rescue ArgumentError
50
+ raise CorruptMessage
51
+ end
52
+
53
+ zstream = Zlib::Inflate.new(-Zlib::MAX_WBITS)
54
+ xml = ''
55
+ begin
56
+ # do it in 1K slices, so we can protect against bombs
57
+ (0..deflated.bytesize / 1024).each do |i|
58
+ xml.concat(zstream.inflate(deflated.byteslice(i * 1024, 1024)))
59
+ raise MessageTooLarge if xml.bytesize > SAML2.config[:max_message_size]
60
+ end
61
+ xml.concat(zstream.finish)
62
+ raise MessageTooLarge if xml.bytesize > SAML2.config[:max_message_size]
63
+ rescue Zlib::DataError, Zlib::BufError
64
+ raise CorruptMessage
65
+ end
66
+
67
+ zstream.close
68
+ message = Message.parse(xml)
69
+ # if a block is provided, it's to fetch the proper certificate
70
+ # based on the contents of the message
71
+ public_key ||= yield(message, sig_alg) if block_given?
72
+ if public_key
73
+ raise UnsignedMessage unless signature
74
+ raise UnsupportedSignatureAlgorithm unless SigAlgs::RECOGNIZED.include?(sig_alg)
75
+
76
+ begin
77
+ signature = Base64.strict_decode64(signature)
78
+ rescue ArgumentError
79
+ raise CorruptMessage
80
+ end
81
+
82
+ base_string = find_raw_query_param(uri.query, message_param)
83
+ base_string << '&' << find_raw_query_param(uri.query, 'RelayState') if relay_state
84
+ base_string << '&' << find_raw_query_param(uri.query, 'SigAlg')
85
+
86
+ valid_signature = false
87
+ # there could be multiple certificates to try
88
+ Array(public_key).each do |key|
89
+ if key.verify(OpenSSL::Digest::SHA1.new, signature, base_string)
90
+ # notify the caller which certificate was used
91
+ public_key_used&.call(key)
92
+ valid_signature = true
93
+ break
94
+ end
95
+ end
96
+ raise InvalidSignature unless valid_signature
97
+ end
98
+ [message, relay_state]
99
+ end
100
+
101
+ def encode(message, relay_state: nil, private_key: nil)
102
+ result = URI.parse(message.destination)
103
+ original_query = URI.decode_www_form(result.query) if result.query
104
+ original_query ||= []
105
+ # remove any SAML protocol parameters
106
+ %w{SAMLEncoding SAMLRequest SAMLResponse RelayState SigAlg Signature}.each do |param|
107
+ original_query.delete_if { |(k, v)| k == param }
108
+ end
109
+
110
+ xml = message.to_s
111
+ zstream = Zlib::Deflate.new(Zlib::BEST_COMPRESSION, -Zlib::MAX_WBITS)
112
+ deflated = zstream.deflate(xml, Zlib::FINISH)
113
+ zstream.close
114
+ base64 = Base64.strict_encode64(deflated)
115
+
116
+ query = []
117
+ query << [message.is_a?(Request) ? 'SAMLRequest' : 'SAMLResponse', base64]
118
+ query << ['RelayState', relay_state] if relay_state
119
+ if private_key
120
+ query << ['SigAlg', SigAlgs::RSA_SHA1]
121
+ base_string = URI.encode_www_form(query)
122
+ signature = private_key.sign(OpenSSL::Digest::SHA1.new, base_string)
123
+ query << ['Signature', Base64.strict_encode64(signature)]
124
+ end
125
+
126
+ result.query = URI.encode_www_form(original_query + query)
127
+ result.to_s
128
+ end
129
+
130
+ private
131
+
132
+ # we need to find the param, and return it still encoded from the URL
133
+ def find_raw_query_param(query, param)
134
+ start = query.index(param)
135
+ finish = (query.index('&', start + param.length + 1) || 0) - 1
136
+ query[start..finish]
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -1,7 +1,7 @@
1
1
  require 'saml2/base'
2
2
 
3
3
  module SAML2
4
- class Contact
4
+ class Contact < Base
5
5
  module Type
6
6
  ADMINISTRATIVE = 'administrative'.freeze
7
7
  BILLING = 'billing'.freeze
@@ -12,27 +12,25 @@ module SAML2
12
12
 
13
13
  attr_accessor :type, :company, :given_name, :surname, :email_addresses, :telephone_numbers
14
14
 
15
- def self.from_xml(node)
16
- return nil unless node
17
-
18
- result = new(node['contactType'])
19
- company = node.at_xpath('md:Company', Namespaces::ALL)
20
- result.company = company && company.content && company.content.strip
21
- given_name = node.at_xpath('md:GivenName', Namespaces::ALL)
22
- result.given_name = given_name && given_name.content && given_name.content.strip
23
- surname = node.at_xpath('md:SurName', Namespaces::ALL)
24
- result.surname = surname && surname.content && surname.content.strip
25
- result.email_addresses = Base.load_string_array(node, 'md:EmailAddress')
26
- result.telephone_numbers = Base.load_string_array(node, 'md:TelephoneNumber')
27
- result
28
- end
29
-
30
15
  def initialize(type = Type::OTHER)
31
16
  @type = type
32
17
  @email_addresses = []
33
18
  @telephone_numbers = []
34
19
  end
35
20
 
21
+ def from_xml(node)
22
+ self.type = node['contactType']
23
+ company = node.at_xpath('md:Company', Namespaces::ALL)
24
+ self.company = company && company.content && company.content.strip
25
+ given_name = node.at_xpath('md:GivenName', Namespaces::ALL)
26
+ self.given_name = given_name && given_name.content && given_name.content.strip
27
+ surname = node.at_xpath('md:SurName', Namespaces::ALL)
28
+ self.surname = surname && surname.content && surname.content.strip
29
+ self.email_addresses = load_string_array(node, 'md:EmailAddress')
30
+ self.telephone_numbers = load_string_array(node, 'md:TelephoneNumber')
31
+ self
32
+ end
33
+
36
34
  def build(builder)
37
35
  builder['md'].ContactPerson('contactType' => type) do |contact_person|
38
36
  contact_person['md'].Company(company) if company
@@ -1,12 +1,10 @@
1
+ require 'saml2/bindings/http_redirect'
2
+
1
3
  module SAML2
2
4
  class Endpoint < Base
3
5
  module Bindings
4
6
  HTTP_POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST".freeze
5
- HTTP_REDIRECT = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect".freeze
6
- end
7
-
8
- module Encodings
9
- DEFLATE= "urn:oasis:names:tc:SAML:2.0:bindings:URL-Encoding:DEFLATE".freeze
7
+ HTTP_REDIRECT = ::SAML2::Bindings::HTTPRedirect::URN
10
8
  end
11
9
 
12
10
  attr_reader :location, :binding
@@ -20,6 +18,7 @@ module SAML2
20
18
  end
21
19
 
22
20
  def from_xml(node)
21
+ super
23
22
  @location = node['Location']
24
23
  @binding = node['Binding']
25
24
  end
@@ -31,7 +30,7 @@ module SAML2
31
30
  class Indexed < Endpoint
32
31
  include IndexedObject
33
32
 
34
- def initialize(location = nil, index = nil, is_default = false, binding = Bindings::HTTP_POST)
33
+ def initialize(location = nil, index = nil, is_default = nil, binding = Bindings::HTTP_POST)
35
34
  super(location, binding)
36
35
  @index, @is_default = index, is_default
37
36
  end
@@ -26,34 +26,40 @@ module SAML2
26
26
  end
27
27
  end
28
28
 
29
- class Group < Array
30
- def self.from_xml(node)
31
- node && new.from_xml(node)
29
+ class Group < Base
30
+ include Enumerable
31
+ [:each, :[]].each do |method|
32
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
33
+ def #{method}(*args, &block)
34
+ @entities.#{method}(*args, &block)
35
+ end
36
+ RUBY
32
37
  end
33
38
 
34
39
  def initialize
40
+ @entities = []
35
41
  @valid_until = nil
36
42
  end
37
43
 
38
44
  def from_xml(node)
39
- @root = node
45
+ super
40
46
  remove_instance_variable(:@valid_until)
41
- replace(Base.load_object_array(@root, "md:EntityDescriptor|md:EntitiesDescriptor",
47
+ @entities = Base.load_object_array(xml, "md:EntityDescriptor|md:EntitiesDescriptor",
42
48
  'EntityDescriptor' => Entity,
43
- 'EntitiesDescriptor' => Group))
49
+ 'EntitiesDescriptor' => Group)
44
50
  end
45
51
 
46
52
  def valid_schema?
47
- Schemas.federation.valid?(@root.document)
53
+ Schemas.federation.valid?(xml.document)
48
54
  end
49
55
 
50
56
  def signature
51
57
  unless instance_variable_defined?(:@signature)
52
- @signature = @root.at_xpath('dsig:Signature', Namespaces::ALL)
58
+ @signature = xml.at_xpath('dsig:Signature', Namespaces::ALL)
53
59
  signed_node = @signature.at_xpath('dsig:SignedInfo/dsig:Reference', Namespaces::ALL)['URI']
54
60
  # validating the schema will automatically add ID attributes, so check that first
55
- @root.set_id_attribute('ID') unless @root.document.get_id(@root['ID'])
56
- @signature = nil unless signed_node == "##{@root['ID']}"
61
+ xml.set_id_attribute('ID') unless xml.document.get_id(xml['ID'])
62
+ @signature = nil unless signed_node == "##{xml['ID']}"
57
63
  end
58
64
  @signature
59
65
  end
@@ -68,7 +74,7 @@ module SAML2
68
74
 
69
75
  def valid_until
70
76
  unless instance_variable_defined?(:@valid_until)
71
- @valid_until = @root['validUntil'] && Time.parse(@root['validUntil'])
77
+ @valid_until = xml['validUntil'] && Time.parse(xml['validUntil'])
72
78
  end
73
79
  @valid_until
74
80
  end
@@ -82,23 +88,22 @@ module SAML2
82
88
  end
83
89
 
84
90
  def from_xml(node)
85
- @root = node
91
+ super
86
92
  remove_instance_variable(:@valid_until)
87
93
  @roles = nil
88
- super
89
94
  end
90
95
 
91
96
  def valid_schema?
92
- Schemas.federation.valid?(@root.document)
97
+ Schemas.federation.valid?(xml.document)
93
98
  end
94
99
 
95
100
  def entity_id
96
- @entity_id || @root && @root['entityID']
101
+ @entity_id || xml && xml['entityID']
97
102
  end
98
103
 
99
104
  def valid_until
100
105
  unless instance_variable_defined?(:@valid_until)
101
- @valid_until = @root['validUntil'] && Time.parse(@root['validUntil'])
106
+ @valid_until = xml['validUntil'] && Time.parse(xml['validUntil'])
102
107
  end
103
108
  @valid_until
104
109
  end
@@ -112,8 +117,8 @@ module SAML2
112
117
  end
113
118
 
114
119
  def roles
115
- @roles ||= load_object_array(@root, 'md:IDPSSODescriptor', IdentityProvider) +
116
- load_object_array(@root, 'md:SPSSODescriptor', ServiceProvider)
120
+ @roles ||= load_object_array(xml, 'md:IDPSSODescriptor', IdentityProvider) +
121
+ load_object_array(xml, 'md:SPSSODescriptor', ServiceProvider)
117
122
  end
118
123
 
119
124
  def build(builder)