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.
- checksums.yaml +4 -4
- data/LICENSE.md +22 -0
- data/README.md +35 -27
- data/lib/lyrebird/assertion.rb +82 -46
- data/lib/lyrebird/certificate.rb +30 -21
- data/lib/lyrebird/defaults.rb +1 -1
- data/lib/lyrebird/encryption.rb +54 -33
- data/lib/lyrebird/response.rb +52 -25
- data/lib/lyrebird/signature.rb +86 -38
- data/lib/lyrebird/version.rb +1 -1
- data/lib/lyrebird.rb +16 -1
- data/lyrebird.gemspec +8 -1
- data/test/lyrebird/assertion_test.rb +115 -96
- data/test/lyrebird/certificate_test.rb +70 -37
- data/test/lyrebird/defaults_test.rb +47 -0
- data/test/lyrebird/encryption_test.rb +48 -35
- data/test/lyrebird/id_test.rb +7 -0
- data/test/lyrebird/integration_test.rb +243 -0
- data/test/lyrebird/namespaces_test.rb +63 -0
- data/test/lyrebird/response_test.rb +67 -57
- data/test/lyrebird/signature_test.rb +77 -43
- metadata +40 -8
data/lib/lyrebird/signature.rb
CHANGED
|
@@ -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.
|
|
12
|
-
|
|
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
|
|
19
|
-
|
|
20
|
-
sig.
|
|
21
|
-
sig.
|
|
22
|
-
|
|
23
|
-
sig.
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
si.
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
ref.
|
|
54
|
-
ref
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
71
|
-
|
|
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
|
data/lib/lyrebird/version.rb
CHANGED
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
41
|
+
assert @root["ID"].start_with?("_")
|
|
29
42
|
end
|
|
30
43
|
|
|
31
44
|
def test_root_version
|
|
32
|
-
assert_equal "2.0", @root
|
|
45
|
+
assert_equal "2.0", @root["Version"]
|
|
33
46
|
end
|
|
34
47
|
|
|
35
48
|
def test_root_issue_instant
|
|
36
|
-
instant = Time.iso8601(@root
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
67
|
-
assert_equal NAMEID_EMAIL, name_id
|
|
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.
|
|
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.
|
|
83
|
-
assert_equal format, name_id
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
119
|
-
sc = subject.
|
|
120
|
-
scd = sc.
|
|
121
|
-
assert_equal recipient, scd
|
|
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.
|
|
129
|
-
sc = subject.
|
|
130
|
-
scd = sc.
|
|
131
|
-
assert_equal in_response_to, scd
|
|
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.
|
|
137
|
-
sc = subject.
|
|
138
|
-
scd = sc.
|
|
139
|
-
assert_nil scd
|
|
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.
|
|
147
|
-
sc = subject.
|
|
148
|
-
scd = sc.
|
|
149
|
-
not_on_or_after = Time.iso8601(scd
|
|
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
|
|
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.
|
|
168
|
-
assert_equal not_before.iso8601, conditions
|
|
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
|
|
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.
|
|
182
|
-
not_on_or_after = Time.iso8601(conditions
|
|
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.
|
|
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.
|
|
204
|
-
ar = conditions.
|
|
205
|
-
assert_equal audience, ar.
|
|
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
|
|
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
|
|
232
|
+
assert @authn_statement["SessionIndex"].start_with?("_")
|
|
220
233
|
end
|
|
221
234
|
|
|
222
235
|
def test_authn_context
|
|
223
|
-
authn_context = @authn_statement.
|
|
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.
|
|
230
|
-
class_ref = ac.
|
|
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.
|
|
241
|
-
ac = as.
|
|
242
|
-
class_ref = ac.
|
|
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.
|
|
248
|
-
attrs = as.
|
|
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
|
|
252
|
-
assert_equal "Test", first.
|
|
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
|
|
255
|
-
assert_equal "User", last.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
278
|
-
assert_equal ATTR_NAME_FORMAT, attr
|
|
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
|
-
|
|
285
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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.
|
|
311
|
-
attrs = as.
|
|
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
|
|
315
|
-
email_value = email_attr.
|
|
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
|
|
319
|
-
name_value = name_attr.
|
|
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
|
|
323
|
-
group_values = groups_attr.
|
|
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
|