lyrebird 1.0.0.alpha2 → 1.0.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.
@@ -2,73 +2,121 @@
2
2
 
3
3
  module Lyrebird
4
4
  class Signature
5
+ C14N_EXCLUSIVE = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
6
+
5
7
  def initialize(element, certificate)
6
8
  @element = element
7
9
  @certificate = certificate
10
+ @doc = element.document
8
11
  end
9
12
 
10
13
  def sign!
11
- issuer = @element.elements["saml:Issuer"]
12
- @element.insert_after(issuer, signature_element)
14
+ issuer = @element.at_xpath("saml:Issuer", "saml" => SAML_ASSERTION_NS)
15
+ issuer.add_next_sibling(build_signature)
13
16
  self
14
17
  end
15
18
 
16
19
  private
17
20
 
18
- def signature_element
19
- REXML::Element.new("ds:Signature").tap do |sig|
20
- sig.add_namespace("ds", XMLDSIG_NS)
21
- sig.add_element(signed_info)
22
- sig.add_element(signature_value)
23
- sig.add_element(key_info)
21
+ def build_signature
22
+ @doc.create_element("Signature").tap do |sig|
23
+ @ds = sig.add_namespace_definition("ds", XMLDSIG_NS)
24
+ sig.namespace = @ds
25
+
26
+ sig.add_child(build_signed_info)
27
+ sig.add_child(build_signature_value)
28
+ sig.add_child(build_key_info)
24
29
  end
25
30
  end
26
31
 
27
- def signed_info
28
- REXML::Element.new("ds:SignedInfo").tap do |si|
29
- cm = si.add_element("ds:CanonicalizationMethod")
30
- cm.add_attribute("Algorithm", EXC_C14N)
31
- sm = si.add_element("ds:SignatureMethod")
32
- sm.add_attribute("Algorithm", RSA_SHA256)
33
- si.add_element(reference)
32
+ def build_signed_info
33
+ @doc.create_element("SignedInfo").tap do |si|
34
+ @signed_info = si
35
+ si.add_namespace_definition("ds", XMLDSIG_NS)
36
+ si.namespace = @ds
37
+
38
+ si.add_child(@doc.create_element("CanonicalizationMethod")).tap do |cm|
39
+ cm.namespace = @ds
40
+ cm["Algorithm"] = EXC_C14N
41
+ end
42
+
43
+ si.add_child(@doc.create_element("SignatureMethod")).tap do |sm|
44
+ sm.namespace = @ds
45
+ sm["Algorithm"] = RSA_SHA256
46
+ end
47
+
48
+ si.add_child(build_reference)
34
49
  end
35
50
  end
36
51
 
37
- def signature_value
38
- REXML::Element.new("ds:SignatureValue").tap do |sv|
39
- sig = @certificate.private_key.sign("SHA256", signed_info.to_s)
40
- sv.text = Base64.strict_encode64(sig)
52
+ def build_signature_value
53
+ @doc.create_element("SignatureValue").tap do |sv|
54
+ sv.namespace = @ds
55
+ canonical = canonicalize_signed_info
56
+ sig = @certificate.sign(canonical)
57
+ sv.content = Base64.strict_encode64(sig)
41
58
  end
42
59
  end
43
60
 
44
- def key_info
45
- REXML::Element.new("ds:KeyInfo").tap do |ki|
46
- x = ki.add_element("ds:X509Data")
47
- x.add_element("ds:X509Certificate").text = @certificate.base64
61
+ def canonicalize_signed_info
62
+ xml = @signed_info.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
63
+ xml = xml.sub("<ds:SignedInfo>", "<ds:SignedInfo xmlns:ds=\"#{XMLDSIG_NS}\">")
64
+ Nokogiri::XML(xml).root.canonicalize(C14N_EXCLUSIVE)
65
+ end
66
+
67
+ def build_key_info
68
+ @doc.create_element("KeyInfo").tap do |ki|
69
+ ki.namespace = @ds
70
+
71
+ ki.add_child(@doc.create_element("X509Data")).tap do |xd|
72
+ xd.namespace = @ds
73
+
74
+ xd.add_child(@doc.create_element("X509Certificate")).tap do |xc|
75
+ xc.namespace = @ds
76
+ xc.content = @certificate.base64
77
+ end
78
+ end
48
79
  end
49
80
  end
50
81
 
51
- def reference
52
- REXML::Element.new("ds:Reference").tap do |ref|
53
- ref.add_attribute("URI", "##{@element.attributes["ID"]}")
54
- ref.add_element(transforms)
55
- dm = ref.add_element("ds:DigestMethod")
56
- dm.add_attribute("Algorithm", SHA256_DIGEST)
57
- ref.add_element("ds:DigestValue").text = compute_digest(@element)
82
+ def build_reference
83
+ @doc.create_element("Reference").tap do |ref|
84
+ ref.namespace = @ds
85
+ ref["URI"] = "##{@element["ID"]}"
86
+
87
+ ref.add_child(build_transforms)
88
+
89
+ ref.add_child(@doc.create_element("DigestMethod")).tap do |dm|
90
+ dm.namespace = @ds
91
+ dm["Algorithm"] = SHA256_DIGEST
92
+ end
93
+
94
+ ref.add_child(@doc.create_element("DigestValue")).tap do |dv|
95
+ dv.namespace = @ds
96
+ dv.content = compute_digest
97
+ end
58
98
  end
59
99
  end
60
100
 
61
- def transforms
62
- REXML::Element.new("ds:Transforms").tap do |t|
63
- enveloped = t.add_element("ds:Transform")
64
- enveloped.add_attribute("Algorithm", ENVELOPED_SIG)
65
- c14n = t.add_element("ds:Transform")
66
- c14n.add_attribute("Algorithm", EXC_C14N)
101
+ def build_transforms
102
+ @doc.create_element("Transforms").tap do |t|
103
+ t.namespace = @ds
104
+
105
+ t.add_child(@doc.create_element("Transform")).tap do |tr|
106
+ tr.namespace = @ds
107
+ tr["Algorithm"] = ENVELOPED_SIG
108
+ end
109
+
110
+ t.add_child(@doc.create_element("Transform")).tap do |tr|
111
+ tr.namespace = @ds
112
+ tr["Algorithm"] = EXC_C14N
113
+ end
67
114
  end
68
115
  end
69
116
 
70
- def compute_digest(element)
71
- digest = OpenSSL::Digest::SHA256.digest(element.to_s)
117
+ def compute_digest
118
+ canonical = @element.canonicalize(C14N_EXCLUSIVE)
119
+ digest = OpenSSL::Digest::SHA256.digest(canonical)
72
120
  Base64.strict_encode64(digest)
73
121
  end
74
122
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lyrebird
4
- VERSION = "1.0.0.alpha2"
4
+ VERSION = "1.0.0"
5
5
  end
data/lib/lyrebird.rb CHANGED
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "base64"
4
+ require "nokogiri"
4
5
  require "openssl"
5
6
  require "ostruct"
6
- require "rexml"
7
7
  require "securerandom"
8
8
  require "time"
9
9
 
@@ -19,4 +19,19 @@ require_relative "lyrebird/version"
19
19
 
20
20
  module Lyrebird
21
21
  class Error < StandardError; end
22
+
23
+ rails_env = ENV["RAILS_ENV"]&.downcase
24
+ rack_env = ENV["RACK_ENV"]&.downcase
25
+
26
+ if rails_env == "production" || rack_env == "production"
27
+ warn <<~MESSAGE.strip
28
+ [Lyrebird] WARNING: Loaded in production environment. \
29
+ This library is for testing only.
30
+ MESSAGE
31
+ end
32
+
33
+ def self.configure
34
+ yield DEFAULTS
35
+ DEFAULTS.freeze
36
+ end
22
37
  end
data/lyrebird.gemspec CHANGED
@@ -8,6 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["Josh"]
9
9
 
10
10
  spec.summary = "Mimics SAML Identity Provider (IdP) responses for testing"
11
+ spec.homepage = "https://github.com/simplesaml/lyrebird"
12
+ spec.license = "MIT"
11
13
  spec.required_ruby_version = ">= 3.2.0"
12
14
 
13
15
  spec.files = `git ls-files -z`.split("\x0")
@@ -18,9 +20,14 @@ Gem::Specification.new do |spec|
18
20
  spec.require_paths = ["lib"]
19
21
 
20
22
  spec.add_dependency "base64"
23
+ spec.add_dependency "nokogiri"
21
24
  spec.add_dependency "ostruct"
22
- spec.add_dependency "rexml"
23
25
 
26
+ # ruby-saml's gemspec has a conditional dependency on logger for Ruby >= 3.4,
27
+ # but it was evaluated at gem build time (on Ruby < 3.4), so the published gem
28
+ # doesn't include it.
29
+ spec.add_development_dependency "logger"
24
30
  spec.add_development_dependency "minitest"
25
31
  spec.add_development_dependency "rake"
32
+ spec.add_development_dependency "ruby-saml"
26
33
  end
@@ -4,74 +4,87 @@ require "test_helper"
4
4
 
5
5
  module Lyrebird
6
6
  class AssertionTest < Minitest::Test
7
+ NS = { "saml" => SAML_ASSERTION_NS }.freeze
8
+
7
9
  def setup
8
- @assertion = Assertion.new.document
9
- @root = @assertion.root
10
- @subject = @root.elements["saml:Subject"]
11
- @sc = @subject.elements["saml:SubjectConfirmation"]
12
- @scd = @sc.elements["saml:SubjectConfirmationData"]
13
- @conditions = @root.elements["saml:Conditions"]
14
- @audience_restriction = @conditions.elements["saml:AudienceRestriction"]
15
- @authn_statement = @root.elements["saml:AuthnStatement"]
10
+ unless defined?(@@assertion)
11
+ @@assertion = Assertion.new.document
12
+ @@root = @@assertion.root
13
+ @@subject = @@root.at_xpath("saml:Subject", NS)
14
+ @@sc = @@subject.at_xpath("saml:SubjectConfirmation", NS)
15
+ @@scd = @@sc.at_xpath("saml:SubjectConfirmationData", NS)
16
+ @@conditions = @@root.at_xpath("saml:Conditions", NS)
17
+ @@audience_restriction = @@conditions.at_xpath("saml:AudienceRestriction", NS)
18
+ @@authn_statement = @@root.at_xpath("saml:AuthnStatement", NS)
19
+ end
20
+
21
+ @assertion = @@assertion
22
+ @root = @@root
23
+ @subject = @@subject
24
+ @sc = @@sc
25
+ @scd = @@scd
26
+ @conditions = @@conditions
27
+ @audience_restriction = @@audience_restriction
28
+ @authn_statement = @@authn_statement
16
29
  end
17
30
 
18
31
  def test_root_name
19
32
  assert_equal "Assertion", @root.name
20
- assert_equal "saml", @root.prefix
33
+ assert_equal "saml", @root.namespace.prefix
21
34
  end
22
35
 
23
36
  def test_root_namespace
24
- assert_equal SAML_ASSERTION_NS, @root.namespace
37
+ assert_equal SAML_ASSERTION_NS, @root.namespace.href
25
38
  end
26
39
 
27
40
  def test_root_id
28
- assert @root.attributes["ID"].start_with?("_")
41
+ assert @root["ID"].start_with?("_")
29
42
  end
30
43
 
31
44
  def test_root_version
32
- assert_equal "2.0", @root.attributes["Version"]
45
+ assert_equal "2.0", @root["Version"]
33
46
  end
34
47
 
35
48
  def test_root_issue_instant
36
- instant = Time.iso8601(@root.attributes["IssueInstant"])
49
+ instant = Time.iso8601(@root["IssueInstant"])
37
50
  assert_in_delta Time.now.to_i, instant.to_i, 1
38
51
  end
39
52
 
40
53
  def test_issuer
41
- issuer = @root.elements["saml:Issuer"]
54
+ issuer = @root.at_xpath("saml:Issuer", NS)
42
55
  assert_equal "Issuer", issuer.name
43
- assert_equal "saml", issuer.prefix
56
+ assert_equal "saml", issuer.namespace.prefix
44
57
  assert_equal DEFAULTS.issuer, issuer.text
45
58
  end
46
59
 
47
60
  def test_issuer_override
48
61
  assertion = Assertion.new(issuer: "https://test.example.com").document
49
- issuer = assertion.root.elements["saml:Issuer"]
62
+ issuer = assertion.root.at_xpath("saml:Issuer", NS)
50
63
  assert_equal "https://test.example.com", issuer.text
51
64
  end
52
65
 
53
66
  def test_subject
54
67
  assert_equal "Subject", @subject.name
55
- assert_equal "saml", @subject.prefix
68
+ assert_equal "saml", @subject.namespace.prefix
56
69
  end
57
70
 
58
71
  def test_name_id
59
- name_id = @subject.elements["saml:NameID"]
72
+ name_id = @subject.at_xpath("saml:NameID", NS)
60
73
  assert_equal "NameID", name_id.name
61
- assert_equal "saml", name_id.prefix
74
+ assert_equal "saml", name_id.namespace.prefix
62
75
  assert_equal DEFAULTS.name_id, name_id.text
63
76
  end
64
77
 
65
78
  def test_name_id_format_default
66
- name_id = @subject.elements["saml:NameID"]
67
- assert_equal NAMEID_EMAIL, name_id.attributes["Format"]
79
+ name_id = @subject.at_xpath("saml:NameID", NS)
80
+ assert_equal NAMEID_EMAIL, name_id["Format"]
68
81
  end
69
82
 
70
83
  def test_name_id_override
71
84
  email = "user@test.com"
72
85
  refute_equal email, DEFAULTS.name_id
73
86
  assertion = Assertion.new(name_id: email).document
74
- name_id = assertion.root.elements["saml:Subject/saml:NameID"]
87
+ name_id = assertion.root.at_xpath("saml:Subject/saml:NameID", NS)
75
88
  assert_equal email, name_id.text
76
89
  end
77
90
 
@@ -79,97 +92,97 @@ module Lyrebird
79
92
  format = NAMEID_PERSISTENT
80
93
  refute_equal format, DEFAULTS.name_id_format
81
94
  assertion = Assertion.new(name_id_format: format).document
82
- name_id = assertion.root.elements["saml:Subject/saml:NameID"]
83
- assert_equal format, name_id.attributes["Format"]
95
+ name_id = assertion.root.at_xpath("saml:Subject/saml:NameID", NS)
96
+ assert_equal format, name_id["Format"]
84
97
  end
85
98
 
86
99
  def test_subject_confirmation
87
100
  assert_equal "SubjectConfirmation", @sc.name
88
- assert_equal "saml", @sc.prefix
101
+ assert_equal "saml", @sc.namespace.prefix
89
102
  end
90
103
 
91
104
  def test_subject_confirmation_method
92
- assert_equal CM_BEARER, @sc.attributes["Method"]
105
+ assert_equal CM_BEARER, @sc["Method"]
93
106
  end
94
107
 
95
108
  def test_subject_confirmation_data
96
109
  assert_equal "SubjectConfirmationData", @scd.name
97
- assert_equal "saml", @scd.prefix
110
+ assert_equal "saml", @scd.namespace.prefix
98
111
  end
99
112
 
100
113
  def test_valid_for_default
101
- not_on_or_after = Time.iso8601(@scd.attributes["NotOnOrAfter"])
114
+ not_on_or_after = Time.iso8601(@scd["NotOnOrAfter"])
102
115
  expected = Time.now.utc + DEFAULTS.valid_for
103
116
  assert_in_delta expected.to_i, not_on_or_after.to_i, 1
104
117
  end
105
118
 
106
119
  def test_recipient_default
107
- assert_equal DEFAULTS.recipient, @scd.attributes["Recipient"]
120
+ assert_equal DEFAULTS.recipient, @scd["Recipient"]
108
121
  end
109
122
 
110
123
  def test_in_response_to_default
111
- assert_equal DEFAULTS.in_response_to, @scd.attributes["InResponseTo"]
124
+ assert_equal DEFAULTS.in_response_to, @scd["InResponseTo"]
112
125
  end
113
126
 
114
127
  def test_recipient_override
115
128
  recipient = "https://custom.example.com/acs"
116
129
  refute_equal recipient, DEFAULTS.recipient
117
130
  assertion = Assertion.new(recipient: recipient).document
118
- subject = assertion.root.elements["saml:Subject"]
119
- sc = subject.elements["saml:SubjectConfirmation"]
120
- scd = sc.elements["saml:SubjectConfirmationData"]
121
- assert_equal recipient, scd.attributes["Recipient"]
131
+ subject = assertion.root.at_xpath("saml:Subject", NS)
132
+ sc = subject.at_xpath("saml:SubjectConfirmation", NS)
133
+ scd = sc.at_xpath("saml:SubjectConfirmationData", NS)
134
+ assert_equal recipient, scd["Recipient"]
122
135
  end
123
136
 
124
137
  def test_in_response_to_override
125
138
  in_response_to = "_custom_request"
126
139
  refute_equal in_response_to, DEFAULTS.in_response_to
127
140
  assertion = Assertion.new(in_response_to: in_response_to).document
128
- subject = assertion.root.elements["saml:Subject"]
129
- sc = subject.elements["saml:SubjectConfirmation"]
130
- scd = sc.elements["saml:SubjectConfirmationData"]
131
- assert_equal in_response_to, scd.attributes["InResponseTo"]
141
+ subject = assertion.root.at_xpath("saml:Subject", NS)
142
+ sc = subject.at_xpath("saml:SubjectConfirmation", NS)
143
+ scd = sc.at_xpath("saml:SubjectConfirmationData", NS)
144
+ assert_equal in_response_to, scd["InResponseTo"]
132
145
  end
133
146
 
134
147
  def test_in_response_to_omitted_when_nil
135
148
  assertion = Assertion.new(in_response_to: nil).document
136
- subject = assertion.root.elements["saml:Subject"]
137
- sc = subject.elements["saml:SubjectConfirmation"]
138
- scd = sc.elements["saml:SubjectConfirmationData"]
139
- assert_nil scd.attributes["InResponseTo"]
149
+ subject = assertion.root.at_xpath("saml:Subject", NS)
150
+ sc = subject.at_xpath("saml:SubjectConfirmation", NS)
151
+ scd = sc.at_xpath("saml:SubjectConfirmationData", NS)
152
+ assert_nil scd["InResponseTo"]
140
153
  end
141
154
 
142
155
  def test_valid_for_override
143
156
  valid_for = 600 # 10 minutes
144
157
  refute_equal valid_for, DEFAULTS.valid_for
145
158
  assertion = Assertion.new(valid_for: valid_for).document
146
- subject = assertion.root.elements["saml:Subject"]
147
- sc = subject.elements["saml:SubjectConfirmation"]
148
- scd = sc.elements["saml:SubjectConfirmationData"]
149
- not_on_or_after = Time.iso8601(scd.attributes["NotOnOrAfter"])
159
+ subject = assertion.root.at_xpath("saml:Subject", NS)
160
+ sc = subject.at_xpath("saml:SubjectConfirmation", NS)
161
+ scd = sc.at_xpath("saml:SubjectConfirmationData", NS)
162
+ not_on_or_after = Time.iso8601(scd["NotOnOrAfter"])
150
163
  expected = Time.now.utc + valid_for
151
164
  assert_in_delta expected.to_i, not_on_or_after.to_i, 1
152
165
  end
153
166
 
154
167
  def test_conditions
155
168
  assert_equal "Conditions", @conditions.name
156
- assert_equal "saml", @conditions.prefix
169
+ assert_equal "saml", @conditions.namespace.prefix
157
170
  end
158
171
 
159
172
  def test_conditions_not_before
160
- not_before = Time.iso8601(@conditions.attributes["NotBefore"])
173
+ not_before = Time.iso8601(@conditions["NotBefore"])
161
174
  assert_in_delta Time.now.to_i, not_before.to_i, 1
162
175
  end
163
176
 
164
177
  def test_conditions_not_before_override
165
178
  not_before = Time.now.utc - 60
166
179
  assertion = Assertion.new(not_before: not_before).document
167
- conditions = assertion.root.elements["saml:Conditions"]
168
- assert_equal not_before.iso8601, conditions.attributes["NotBefore"]
180
+ conditions = assertion.root.at_xpath("saml:Conditions", NS)
181
+ assert_equal not_before.iso8601, conditions["NotBefore"]
169
182
  end
170
183
 
171
184
  def test_conditions_not_on_or_after
172
- not_on_or_after = Time.iso8601(@conditions.attributes["NotOnOrAfter"])
185
+ not_on_or_after = Time.iso8601(@conditions["NotOnOrAfter"])
173
186
  expected = Time.now.utc + DEFAULTS.valid_for
174
187
  assert_in_delta expected.to_i, not_on_or_after.to_i, 1
175
188
  end
@@ -178,21 +191,21 @@ module Lyrebird
178
191
  valid_for = 600 # 10 minutes
179
192
  refute_equal valid_for, DEFAULTS.valid_for
180
193
  assertion = Assertion.new(valid_for: valid_for).document
181
- conditions = assertion.root.elements["saml:Conditions"]
182
- not_on_or_after = Time.iso8601(conditions.attributes["NotOnOrAfter"])
194
+ conditions = assertion.root.at_xpath("saml:Conditions", NS)
195
+ not_on_or_after = Time.iso8601(conditions["NotOnOrAfter"])
183
196
  expected = Time.now.utc + valid_for
184
197
  assert_in_delta expected.to_i, not_on_or_after.to_i, 1
185
198
  end
186
199
 
187
200
  def test_audience_restriction
188
201
  assert_equal "AudienceRestriction", @audience_restriction.name
189
- assert_equal "saml", @audience_restriction.prefix
202
+ assert_equal "saml", @audience_restriction.namespace.prefix
190
203
  end
191
204
 
192
205
  def test_audience
193
- audience = @audience_restriction.elements["saml:Audience"]
206
+ audience = @audience_restriction.at_xpath("saml:Audience", NS)
194
207
  assert_equal "Audience", audience.name
195
- assert_equal "saml", audience.prefix
208
+ assert_equal "saml", audience.namespace.prefix
196
209
  assert_equal DEFAULTS.audience, audience.text
197
210
  end
198
211
 
@@ -200,36 +213,36 @@ module Lyrebird
200
213
  audience = "https://custom.sp.example.com"
201
214
  refute_equal audience, DEFAULTS.audience
202
215
  assertion = Assertion.new(audience: audience).document
203
- conditions = assertion.root.elements["saml:Conditions"]
204
- ar = conditions.elements["saml:AudienceRestriction"]
205
- assert_equal audience, ar.elements["saml:Audience"].text
216
+ conditions = assertion.root.at_xpath("saml:Conditions", NS)
217
+ ar = conditions.at_xpath("saml:AudienceRestriction", NS)
218
+ assert_equal audience, ar.at_xpath("saml:Audience", NS).text
206
219
  end
207
220
 
208
221
  def test_authn_statement
209
222
  assert_equal "AuthnStatement", @authn_statement.name
210
- assert_equal "saml", @authn_statement.prefix
223
+ assert_equal "saml", @authn_statement.namespace.prefix
211
224
  end
212
225
 
213
226
  def test_authn_statement_authn_instant
214
- authn_instant = Time.iso8601(@authn_statement.attributes["AuthnInstant"])
227
+ authn_instant = Time.iso8601(@authn_statement["AuthnInstant"])
215
228
  assert_in_delta Time.now.to_i, authn_instant.to_i, 1
216
229
  end
217
230
 
218
231
  def test_authn_statement_session_index
219
- assert @authn_statement.attributes["SessionIndex"].start_with?("_")
232
+ assert @authn_statement["SessionIndex"].start_with?("_")
220
233
  end
221
234
 
222
235
  def test_authn_context
223
- authn_context = @authn_statement.elements["saml:AuthnContext"]
236
+ authn_context = @authn_statement.at_xpath("saml:AuthnContext", NS)
224
237
  assert_equal "AuthnContext", authn_context.name
225
- assert_equal "saml", authn_context.prefix
238
+ assert_equal "saml", authn_context.namespace.prefix
226
239
  end
227
240
 
228
241
  def test_authn_context_class_ref
229
- ac = @authn_statement.elements["saml:AuthnContext"]
230
- class_ref = ac.elements["saml:AuthnContextClassRef"]
242
+ ac = @authn_statement.at_xpath("saml:AuthnContext", NS)
243
+ class_ref = ac.at_xpath("saml:AuthnContextClassRef", NS)
231
244
  assert_equal "AuthnContextClassRef", class_ref.name
232
- assert_equal "saml", class_ref.prefix
245
+ assert_equal "saml", class_ref.namespace.prefix
233
246
  assert_equal DEFAULTS.authn_context, class_ref.text
234
247
  end
235
248
 
@@ -237,62 +250,68 @@ module Lyrebird
237
250
  custom_ref = "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
238
251
  refute_equal custom_ref, DEFAULTS.authn_context
239
252
  assertion = Assertion.new(authn_context: custom_ref).document
240
- as = assertion.root.elements["saml:AuthnStatement"]
241
- ac = as.elements["saml:AuthnContext"]
242
- class_ref = ac.elements["saml:AuthnContextClassRef"]
253
+ as = assertion.root.at_xpath("saml:AuthnStatement", NS)
254
+ ac = as.at_xpath("saml:AuthnContext", NS)
255
+ class_ref = ac.at_xpath("saml:AuthnContextClassRef", NS)
243
256
  assert_equal custom_ref, class_ref.text
244
257
  end
245
258
 
246
259
  def test_default_attributes
247
- as = @root.elements["saml:AttributeStatement"]
248
- attrs = as.elements.to_a("saml:Attribute")
260
+ as = @root.at_xpath("saml:AttributeStatement", NS)
261
+ attrs = as.xpath("saml:Attribute", NS)
249
262
  assert_equal 2, attrs.size
250
263
 
251
- first = attrs.find { |a| a.attributes["Name"] == "first_name" }
252
- assert_equal "Test", first.elements["saml:AttributeValue"].text
264
+ first = attrs.find { |a| a["Name"] == "first_name" }
265
+ assert_equal "Test", first.at_xpath("saml:AttributeValue", NS).text
253
266
 
254
- last = attrs.find { |a| a.attributes["Name"] == "last_name" }
255
- assert_equal "User", last.elements["saml:AttributeValue"].text
267
+ last = attrs.find { |a| a["Name"] == "last_name" }
268
+ assert_equal "User", last.at_xpath("saml:AttributeValue", NS).text
256
269
  end
257
270
 
258
271
  def test_no_attribute_statement_when_empty
259
272
  assertion = Assertion.new(attributes: {}).document
260
- assert_nil assertion.root.elements["saml:AttributeStatement"]
273
+ assert_nil assertion.root.at_xpath("saml:AttributeStatement", NS)
261
274
  end
262
275
 
263
276
  def test_attribute_statement_with_single_value
264
277
  attributes = { "email" => "user@example.com" }
265
278
  assertion = Assertion.new(attributes: attributes).document
266
- as = assertion.root.elements["saml:AttributeStatement"]
279
+ as = assertion.root.at_xpath("saml:AttributeStatement", NS)
267
280
  assert_equal "AttributeStatement", as.name
268
- assert_equal "saml", as.prefix
281
+ assert_equal "saml", as.namespace.prefix
269
282
  end
270
283
 
271
284
  def test_attribute_name_and_format
272
285
  attributes = { "email" => "user@example.com" }
273
286
  assertion = Assertion.new(attributes: attributes).document
274
- attr = assertion.root.elements["saml:AttributeStatement/saml:Attribute"]
287
+ path = "saml:AttributeStatement/saml:Attribute"
288
+ attr = assertion.root.at_xpath(path, NS)
289
+
275
290
  assert_equal "Attribute", attr.name
276
- assert_equal "saml", attr.prefix
277
- assert_equal "email", attr.attributes["Name"]
278
- assert_equal ATTR_NAME_FORMAT, attr.attributes["NameFormat"]
291
+ assert_equal "saml", attr.namespace.prefix
292
+ assert_equal "email", attr["Name"]
293
+ assert_equal ATTR_NAME_FORMAT, attr["NameFormat"]
279
294
  end
280
295
 
281
296
  def test_attribute_single_value
282
297
  attributes = { "email" => "user@example.com" }
283
298
  assertion = Assertion.new(attributes: attributes).document
284
- attr = assertion.root.elements["saml:AttributeStatement/saml:Attribute"]
285
- value = attr.elements["saml:AttributeValue"]
299
+ path = "saml:AttributeStatement/saml:Attribute"
300
+ attr = assertion.root.at_xpath(path, NS)
301
+ value = attr.at_xpath("saml:AttributeValue", NS)
302
+
286
303
  assert_equal "AttributeValue", value.name
287
- assert_equal "saml", value.prefix
304
+ assert_equal "saml", value.namespace.prefix
288
305
  assert_equal "user@example.com", value.text
289
306
  end
290
307
 
291
308
  def test_attribute_multi_value
292
309
  attributes = { "groups" => ["admin", "users", "developers"] }
293
310
  assertion = Assertion.new(attributes: attributes).document
294
- attr = assertion.root.elements["saml:AttributeStatement/saml:Attribute"]
295
- values = attr.elements.to_a("saml:AttributeValue")
311
+ path = "saml:AttributeStatement/saml:Attribute"
312
+ attr = assertion.root.at_xpath(path, NS)
313
+ values = attr.xpath("saml:AttributeValue", NS)
314
+
296
315
  assert_equal 3, values.size
297
316
  assert_equal "admin", values[0].text
298
317
  assert_equal "users", values[1].text
@@ -307,20 +326,20 @@ module Lyrebird
307
326
  }
308
327
 
309
328
  assertion = Assertion.new(attributes: attributes).document
310
- as = assertion.root.elements["saml:AttributeStatement"]
311
- attrs = as.elements.to_a("saml:Attribute")
329
+ as = assertion.root.at_xpath("saml:AttributeStatement", NS)
330
+ attrs = as.xpath("saml:Attribute", NS)
312
331
  assert_equal 3, attrs.size
313
332
 
314
- email_attr = attrs.find { |a| a.attributes["Name"] == "email" }
315
- email_value = email_attr.elements["saml:AttributeValue"].text
333
+ email_attr = attrs.find { |a| a["Name"] == "email" }
334
+ email_value = email_attr.at_xpath("saml:AttributeValue", NS).text
316
335
  assert_equal "user@example.com", email_value
317
336
 
318
- name_attr = attrs.find { |a| a.attributes["Name"] == "name" }
319
- name_value = name_attr.elements["saml:AttributeValue"].text
337
+ name_attr = attrs.find { |a| a["Name"] == "name" }
338
+ name_value = name_attr.at_xpath("saml:AttributeValue", NS).text
320
339
  assert_equal "Test User", name_value
321
340
 
322
- groups_attr = attrs.find { |a| a.attributes["Name"] == "groups" }
323
- group_values = groups_attr.elements.to_a("saml:AttributeValue")
341
+ groups_attr = attrs.find { |a| a["Name"] == "groups" }
342
+ group_values = groups_attr.xpath("saml:AttributeValue", NS)
324
343
  assert_equal 2, group_values.size
325
344
  assert_equal "admin", group_values[0].text
326
345
  assert_equal "users", group_values[1].text