lyrebird 0.0.0 → 1.0.0.alpha1

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.
@@ -0,0 +1,314 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ module Lyrebird
6
+ class AssertionTest < Minitest::Test
7
+ 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"]
16
+ end
17
+
18
+ def test_root_name
19
+ assert_equal "Assertion", @root.name
20
+ assert_equal "saml", @root.prefix
21
+ end
22
+
23
+ def test_root_namespace
24
+ assert_equal SAML_ASSERTION_NS, @root.namespace
25
+ end
26
+
27
+ def test_root_id
28
+ assert @root.attributes["ID"].start_with?("_")
29
+ end
30
+
31
+ def test_root_version
32
+ assert_equal "2.0", @root.attributes["Version"]
33
+ end
34
+
35
+ def test_root_issue_instant
36
+ instant = Time.iso8601(@root.attributes["IssueInstant"])
37
+ assert_in_delta Time.now.to_i, instant.to_i, 1
38
+ end
39
+
40
+ def test_issuer
41
+ issuer = @root.elements["saml:Issuer"]
42
+ assert_equal "Issuer", issuer.name
43
+ assert_equal "saml", issuer.prefix
44
+ assert_equal DEFAULTS.issuer, issuer.text
45
+ end
46
+
47
+ def test_issuer_override
48
+ assertion = Assertion.new(issuer: "https://test.example.com").document
49
+ issuer = assertion.root.elements["saml:Issuer"]
50
+ assert_equal "https://test.example.com", issuer.text
51
+ end
52
+
53
+ def test_subject
54
+ assert_equal "Subject", @subject.name
55
+ assert_equal "saml", @subject.prefix
56
+ end
57
+
58
+ def test_name_id
59
+ name_id = @subject.elements["saml:NameID"]
60
+ assert_equal "NameID", name_id.name
61
+ assert_equal "saml", name_id.prefix
62
+ assert_equal DEFAULTS.name_id, name_id.text
63
+ end
64
+
65
+ def test_name_id_format_default
66
+ name_id = @subject.elements["saml:NameID"]
67
+ assert_equal NAMEID_EMAIL, name_id.attributes["Format"]
68
+ end
69
+
70
+ def test_name_id_override
71
+ email = "user@test.com"
72
+ refute_equal email, DEFAULTS.name_id
73
+ assertion = Assertion.new(name_id: email).document
74
+ name_id = assertion.root.elements["saml:Subject/saml:NameID"]
75
+ assert_equal email, name_id.text
76
+ end
77
+
78
+ def test_name_id_format_override
79
+ format = NAMEID_PERSISTENT
80
+ refute_equal format, DEFAULTS.name_id_format
81
+ 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"]
84
+ end
85
+
86
+ def test_subject_confirmation
87
+ assert_equal "SubjectConfirmation", @sc.name
88
+ assert_equal "saml", @sc.prefix
89
+ end
90
+
91
+ def test_subject_confirmation_method
92
+ assert_equal CM_BEARER, @sc.attributes["Method"]
93
+ end
94
+
95
+ def test_subject_confirmation_data
96
+ assert_equal "SubjectConfirmationData", @scd.name
97
+ assert_equal "saml", @scd.prefix
98
+ end
99
+
100
+ def test_valid_for_default
101
+ not_on_or_after = Time.iso8601(@scd.attributes["NotOnOrAfter"])
102
+ expected = Time.now.utc + DEFAULTS.valid_for
103
+ assert_in_delta expected.to_i, not_on_or_after.to_i, 1
104
+ end
105
+
106
+ def test_recipient_default
107
+ assert_equal DEFAULTS.recipient, @scd.attributes["Recipient"]
108
+ end
109
+
110
+ def test_in_response_to_default
111
+ assert_equal DEFAULTS.in_response_to, @scd.attributes["InResponseTo"]
112
+ end
113
+
114
+ def test_recipient_override
115
+ recipient = "https://custom.example.com/acs"
116
+ refute_equal recipient, DEFAULTS.recipient
117
+ 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"]
122
+ end
123
+
124
+ def test_in_response_to_override
125
+ in_response_to = "_custom_request"
126
+ refute_equal in_response_to, DEFAULTS.in_response_to
127
+ 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"]
132
+ end
133
+
134
+ def test_valid_for_override
135
+ valid_for = 600 # 10 minutes
136
+ refute_equal valid_for, DEFAULTS.valid_for
137
+ assertion = Assertion.new(valid_for: valid_for).document
138
+ subject = assertion.root.elements["saml:Subject"]
139
+ sc = subject.elements["saml:SubjectConfirmation"]
140
+ scd = sc.elements["saml:SubjectConfirmationData"]
141
+ not_on_or_after = Time.iso8601(scd.attributes["NotOnOrAfter"])
142
+ expected = Time.now.utc + valid_for
143
+ assert_in_delta expected.to_i, not_on_or_after.to_i, 1
144
+ end
145
+
146
+ def test_conditions
147
+ assert_equal "Conditions", @conditions.name
148
+ assert_equal "saml", @conditions.prefix
149
+ end
150
+
151
+ def test_conditions_not_before
152
+ not_before = Time.iso8601(@conditions.attributes["NotBefore"])
153
+ assert_in_delta Time.now.to_i, not_before.to_i, 1
154
+ end
155
+
156
+ def test_conditions_not_on_or_after
157
+ not_on_or_after = Time.iso8601(@conditions.attributes["NotOnOrAfter"])
158
+ expected = Time.now.utc + DEFAULTS.valid_for
159
+ assert_in_delta expected.to_i, not_on_or_after.to_i, 1
160
+ end
161
+
162
+ def test_conditions_valid_for_override
163
+ valid_for = 600 # 10 minutes
164
+ refute_equal valid_for, DEFAULTS.valid_for
165
+ assertion = Assertion.new(valid_for: valid_for).document
166
+ conditions = assertion.root.elements["saml:Conditions"]
167
+ not_on_or_after = Time.iso8601(conditions.attributes["NotOnOrAfter"])
168
+ expected = Time.now.utc + valid_for
169
+ assert_in_delta expected.to_i, not_on_or_after.to_i, 1
170
+ end
171
+
172
+ def test_audience_restriction
173
+ assert_equal "AudienceRestriction", @audience_restriction.name
174
+ assert_equal "saml", @audience_restriction.prefix
175
+ end
176
+
177
+ def test_audience
178
+ audience = @audience_restriction.elements["saml:Audience"]
179
+ assert_equal "Audience", audience.name
180
+ assert_equal "saml", audience.prefix
181
+ assert_equal DEFAULTS.audience, audience.text
182
+ end
183
+
184
+ def test_audience_override
185
+ audience = "https://custom.sp.example.com"
186
+ refute_equal audience, DEFAULTS.audience
187
+ assertion = Assertion.new(audience: audience).document
188
+ conditions = assertion.root.elements["saml:Conditions"]
189
+ ar = conditions.elements["saml:AudienceRestriction"]
190
+ assert_equal audience, ar.elements["saml:Audience"].text
191
+ end
192
+
193
+ def test_authn_statement
194
+ assert_equal "AuthnStatement", @authn_statement.name
195
+ assert_equal "saml", @authn_statement.prefix
196
+ end
197
+
198
+ def test_authn_statement_authn_instant
199
+ authn_instant = Time.iso8601(@authn_statement.attributes["AuthnInstant"])
200
+ assert_in_delta Time.now.to_i, authn_instant.to_i, 1
201
+ end
202
+
203
+ def test_authn_statement_session_index
204
+ assert @authn_statement.attributes["SessionIndex"].start_with?("_")
205
+ end
206
+
207
+ def test_authn_context
208
+ authn_context = @authn_statement.elements["saml:AuthnContext"]
209
+ assert_equal "AuthnContext", authn_context.name
210
+ assert_equal "saml", authn_context.prefix
211
+ end
212
+
213
+ def test_authn_context_class_ref
214
+ ac = @authn_statement.elements["saml:AuthnContext"]
215
+ class_ref = ac.elements["saml:AuthnContextClassRef"]
216
+ assert_equal "AuthnContextClassRef", class_ref.name
217
+ assert_equal "saml", class_ref.prefix
218
+ assert_equal DEFAULTS.authn_context, class_ref.text
219
+ end
220
+
221
+ def test_authn_context_override
222
+ custom_ref = "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
223
+ refute_equal custom_ref, DEFAULTS.authn_context
224
+ assertion = Assertion.new(authn_context: custom_ref).document
225
+ as = assertion.root.elements["saml:AuthnStatement"]
226
+ ac = as.elements["saml:AuthnContext"]
227
+ class_ref = ac.elements["saml:AuthnContextClassRef"]
228
+ assert_equal custom_ref, class_ref.text
229
+ end
230
+
231
+ def test_default_attributes
232
+ as = @root.elements["saml:AttributeStatement"]
233
+ attrs = as.elements.to_a("saml:Attribute")
234
+ assert_equal 2, attrs.size
235
+
236
+ first = attrs.find { |a| a.attributes["Name"] == "first_name" }
237
+ assert_equal "Test", first.elements["saml:AttributeValue"].text
238
+
239
+ last = attrs.find { |a| a.attributes["Name"] == "last_name" }
240
+ assert_equal "User", last.elements["saml:AttributeValue"].text
241
+ end
242
+
243
+ def test_no_attribute_statement_when_empty
244
+ assertion = Assertion.new(attributes: {}).document
245
+ assert_nil assertion.root.elements["saml:AttributeStatement"]
246
+ end
247
+
248
+ def test_attribute_statement_with_single_value
249
+ attributes = { "email" => "user@example.com" }
250
+ assertion = Assertion.new(attributes: attributes).document
251
+ as = assertion.root.elements["saml:AttributeStatement"]
252
+ assert_equal "AttributeStatement", as.name
253
+ assert_equal "saml", as.prefix
254
+ end
255
+
256
+ def test_attribute_name_and_format
257
+ attributes = { "email" => "user@example.com" }
258
+ assertion = Assertion.new(attributes: attributes).document
259
+ attr = assertion.root.elements["saml:AttributeStatement/saml:Attribute"]
260
+ assert_equal "Attribute", attr.name
261
+ assert_equal "saml", attr.prefix
262
+ assert_equal "email", attr.attributes["Name"]
263
+ assert_equal ATTR_NAME_FORMAT, attr.attributes["NameFormat"]
264
+ end
265
+
266
+ def test_attribute_single_value
267
+ attributes = { "email" => "user@example.com" }
268
+ assertion = Assertion.new(attributes: attributes).document
269
+ attr = assertion.root.elements["saml:AttributeStatement/saml:Attribute"]
270
+ value = attr.elements["saml:AttributeValue"]
271
+ assert_equal "AttributeValue", value.name
272
+ assert_equal "saml", value.prefix
273
+ assert_equal "user@example.com", value.text
274
+ end
275
+
276
+ def test_attribute_multi_value
277
+ attributes = { "groups" => ["admin", "users", "developers"] }
278
+ assertion = Assertion.new(attributes: attributes).document
279
+ attr = assertion.root.elements["saml:AttributeStatement/saml:Attribute"]
280
+ values = attr.elements.to_a("saml:AttributeValue")
281
+ assert_equal 3, values.size
282
+ assert_equal "admin", values[0].text
283
+ assert_equal "users", values[1].text
284
+ assert_equal "developers", values[2].text
285
+ end
286
+
287
+ def test_multiple_attributes
288
+ attributes = {
289
+ email: "user@example.com",
290
+ name: "Test User",
291
+ groups: ["admin", "users"]
292
+ }
293
+
294
+ assertion = Assertion.new(attributes: attributes).document
295
+ as = assertion.root.elements["saml:AttributeStatement"]
296
+ attrs = as.elements.to_a("saml:Attribute")
297
+ assert_equal 3, attrs.size
298
+
299
+ email_attr = attrs.find { |a| a.attributes["Name"] == "email" }
300
+ email_value = email_attr.elements["saml:AttributeValue"].text
301
+ assert_equal "user@example.com", email_value
302
+
303
+ name_attr = attrs.find { |a| a.attributes["Name"] == "name" }
304
+ name_value = name_attr.elements["saml:AttributeValue"].text
305
+ assert_equal "Test User", name_value
306
+
307
+ groups_attr = attrs.find { |a| a.attributes["Name"] == "groups" }
308
+ group_values = groups_attr.elements.to_a("saml:AttributeValue")
309
+ assert_equal 2, group_values.size
310
+ assert_equal "admin", group_values[0].text
311
+ assert_equal "users", group_values[1].text
312
+ end
313
+ end
314
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ module Lyrebird
6
+ class CertificateTest < Minitest::Test
7
+ def setup
8
+ @certificate = Certificate.generate
9
+ end
10
+
11
+ def test_private_key_is_rsa
12
+ assert_instance_of OpenSSL::PKey::RSA, @certificate.private_key
13
+ end
14
+
15
+ def test_private_key_defaults_to_2048_bits
16
+ assert_equal 2048, @certificate.private_key.n.num_bits
17
+ end
18
+
19
+ def test_private_key_with_custom_bits
20
+ certificate = Certificate.generate(bits: 4096)
21
+ assert_equal 4096, certificate.private_key.n.num_bits
22
+ end
23
+
24
+ def test_certificate_is_x509
25
+ assert_instance_of OpenSSL::X509::Certificate, @certificate.certificate
26
+ end
27
+
28
+ def test_certificate_not_before_is_now
29
+ assert_in_delta Time.now, @certificate.certificate.not_before, 1
30
+ end
31
+
32
+ def test_certificate_not_after_is_one_year_from_now
33
+ one_year = 365 * 24 * 60 * 60
34
+ assert_in_delta Time.now + one_year, @certificate.certificate.not_after, 1
35
+ end
36
+
37
+ def test_certificate_is_signed
38
+ assert @certificate.certificate.verify(@certificate.private_key)
39
+ end
40
+
41
+ def test_certificate_with_custom_subject
42
+ certificate = Certificate.generate(cn: "Test", o: "Acme")
43
+ assert_equal "/CN=Test/O=Acme", certificate.certificate.subject.to_s
44
+ end
45
+
46
+ def test_certificate_with_custom_valid_for
47
+ certificate = Certificate.generate(valid_for: 30).certificate
48
+ thirty_days = 30 * 24 * 60 * 60
49
+ assert_in_delta Time.now + thirty_days, certificate.not_after, 1
50
+ end
51
+
52
+ def test_certificate_with_valid_until
53
+ valid_until = Time.new(2030, 1, 1)
54
+ certificate = Certificate.generate(valid_until: valid_until)
55
+ assert_equal valid_until, certificate.certificate.not_after
56
+ end
57
+
58
+ def test_private_key_pem
59
+ assert @certificate.private_key_pem.start_with?("-----BEGIN")
60
+ end
61
+
62
+ def test_certificate_pem
63
+ assert @certificate.certificate_pem.start_with?("-----BEGIN")
64
+ end
65
+
66
+ def test_fingerprint
67
+ der = @certificate.certificate.to_der
68
+ expected = OpenSSL::Digest::SHA256.hexdigest(der)
69
+ assert_equal expected, @certificate.fingerprint
70
+ end
71
+
72
+ def test_base64
73
+ der = @certificate.certificate.to_der
74
+ expected = Base64.strict_encode64(der)
75
+ assert_equal expected, @certificate.base64
76
+ end
77
+
78
+ def test_load
79
+ certificate = Certificate.load(
80
+ private_key_pem: @certificate.private_key_pem,
81
+ certificate_pem: @certificate.certificate_pem,
82
+ )
83
+
84
+ assert_equal @certificate.fingerprint, certificate.fingerprint
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ module Lyrebird
6
+ class DefaultsTest < Minitest::Test
7
+ def test_issuer
8
+ assert_equal "https://idp.example.com", DEFAULTS.issuer
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ module Lyrebird
6
+ class IDTest < Minitest::Test
7
+ def test_generate_starts_with_underscore
8
+ assert ID.generate.start_with?("_")
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ module Lyrebird
6
+ class ResponseTest < Minitest::Test
7
+ def setup
8
+ @response = Response.new
9
+ @root = @response.document.root
10
+ end
11
+
12
+ def test_mimic_returns_base64
13
+ encoded = @response.mimic
14
+ decoded = Base64.strict_decode64(encoded)
15
+ assert decoded.include?("<samlp:Response")
16
+ assert decoded.include?("<saml:Assertion")
17
+ end
18
+
19
+ def test_root_name
20
+ assert_equal "Response", @root.name
21
+ assert_equal "samlp", @root.prefix
22
+ end
23
+
24
+ def test_root_namespace
25
+ assert_equal SAML_PROTOCOL_NS, @root.namespace
26
+ end
27
+
28
+ def test_saml_namespace_declared
29
+ assert_equal SAML_ASSERTION_NS, @root.namespace("saml")
30
+ end
31
+
32
+ def test_root_id
33
+ assert @root.attributes["ID"].start_with?("_")
34
+ end
35
+
36
+ def test_root_version
37
+ assert_equal "2.0", @root.attributes["Version"]
38
+ end
39
+
40
+ def test_root_issue_instant
41
+ instant = Time.iso8601(@root.attributes["IssueInstant"])
42
+ assert_in_delta Time.now.to_i, instant.to_i, 1
43
+ end
44
+
45
+ def test_destination_default
46
+ assert_equal DEFAULTS.recipient, @root.attributes["Destination"]
47
+ end
48
+
49
+ def test_destination_override
50
+ destination = "https://test.example.com/acs"
51
+ refute_equal destination, DEFAULTS.recipient
52
+ root = Response.new(destination: destination).document.root
53
+ assert_equal destination, root.attributes["Destination"]
54
+ end
55
+
56
+ def test_in_response_to_default
57
+ assert_equal DEFAULTS.in_response_to, @root.attributes["InResponseTo"]
58
+ end
59
+
60
+ def test_in_response_to_override
61
+ in_response_to = "_test_request"
62
+ refute_equal in_response_to, DEFAULTS.in_response_to
63
+ root = Response.new(in_response_to: in_response_to).document.root
64
+ assert_equal in_response_to, root.attributes["InResponseTo"]
65
+ end
66
+
67
+ def test_issuer
68
+ issuer = @root.elements["saml:Issuer"]
69
+ assert_equal "Issuer", issuer.name
70
+ assert_equal "saml", issuer.prefix
71
+ assert_equal DEFAULTS.issuer, issuer.text
72
+ end
73
+
74
+ def test_issuer_override
75
+ url = "https://test.idp.example.com"
76
+ refute_equal url, DEFAULTS.issuer
77
+ root = Response.new(issuer: url).document.root
78
+ issuer = root.elements["saml:Issuer"]
79
+ assert_equal url, issuer.text
80
+ end
81
+
82
+ def test_status
83
+ status = @root.elements["samlp:Status"]
84
+ assert_equal "Status", status.name
85
+ assert_equal "samlp", status.prefix
86
+ end
87
+
88
+ def test_status_code
89
+ status_code = @root.elements["samlp:Status/samlp:StatusCode"]
90
+ assert_equal "StatusCode", status_code.name
91
+ assert_equal "samlp", status_code.prefix
92
+ assert_equal STATUS_SUCCESS, status_code.attributes["Value"]
93
+ end
94
+
95
+ def test_assertion_embedded
96
+ assertion = @root.elements["saml:Assertion"]
97
+ assert_equal "Assertion", assertion.name
98
+ assert_equal "saml", assertion.prefix
99
+ end
100
+
101
+ def test_assertion_has_id
102
+ assertion = @root.elements["saml:Assertion"]
103
+ assert assertion.attributes["ID"].start_with?("_")
104
+ end
105
+
106
+ def test_assertion_inherits_issuer
107
+ url = "https://test.idp.example.com"
108
+ refute_equal url, DEFAULTS.issuer
109
+ root = Response.new(issuer: url).document.root
110
+ assertion = root.elements["saml:Assertion"]
111
+ issuer = assertion.elements["saml:Issuer"]
112
+ assert_equal url, issuer.text
113
+ end
114
+
115
+ def test_assertion_options_flow_through
116
+ email = "test@example.com"
117
+ refute_equal email, DEFAULTS.name_id
118
+ root = Response.new(name_id: email).document.root
119
+ assertion = root.elements["saml:Assertion"]
120
+ name_id = assertion.elements["saml:Subject/saml:NameID"]
121
+ assert_equal email, name_id.text
122
+ end
123
+
124
+ def test_assertion_unsigned_by_default
125
+ assertion = @root.elements["saml:Assertion"]
126
+ assert_nil assertion.elements["ds:Signature"]
127
+ end
128
+
129
+ def test_sign_assertion_adds_signature
130
+ cert = Certificate.generate
131
+ root = Response.new(certificate: cert, sign_assertion: true).document.root
132
+ assertion = root.elements["saml:Assertion"]
133
+ signature = assertion.elements["ds:Signature"]
134
+ assert_equal "Signature", signature.name
135
+ assert_equal "ds", signature.prefix
136
+ end
137
+
138
+ def test_response_unsigned_by_default
139
+ assert_nil @root.elements["ds:Signature"]
140
+ end
141
+
142
+ def test_sign_response_adds_signature
143
+ args = { certificate: Certificate.generate, sign_response: true }
144
+ root = Response.new(**args).document.root
145
+ signature = root.elements["ds:Signature"]
146
+ assert_equal "Signature", signature.name
147
+ assert_equal "ds", signature.prefix
148
+ end
149
+
150
+ def test_sign_both_response_and_assertion
151
+ args = {
152
+ certificate: Certificate.generate,
153
+ sign_assertion: true,
154
+ sign_response: true
155
+ }
156
+
157
+ root = Response.new(**args).document.root
158
+ refute_nil root.elements["ds:Signature"]
159
+ refute_nil root.elements["saml:Assertion/ds:Signature"]
160
+ end
161
+
162
+ def test_sign_neither_response_nor_assertion
163
+ root = Response.new(certificate: Certificate.generate).document.root
164
+ assert_nil root.elements["ds:Signature"]
165
+ assert_nil root.elements["saml:Assertion/ds:Signature"]
166
+ end
167
+ end
168
+ end