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
|
@@ -5,83 +5,116 @@ require "test_helper"
|
|
|
5
5
|
module Lyrebird
|
|
6
6
|
class CertificateTest < Minitest::Test
|
|
7
7
|
def setup
|
|
8
|
-
|
|
8
|
+
unless defined?(@@shared)
|
|
9
|
+
@@shared = Certificate.build
|
|
10
|
+
@@created_at = Time.now.utc
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
@certificate = @@shared
|
|
14
|
+
@created_at = @@created_at
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_key_is_rsa
|
|
18
|
+
assert_instance_of OpenSSL::PKey::RSA, @certificate.key
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_key_defaults_to_2048_bits
|
|
22
|
+
assert_equal 2048, @certificate.key.n.num_bits
|
|
9
23
|
end
|
|
10
24
|
|
|
11
|
-
def
|
|
12
|
-
|
|
25
|
+
def test_key_with_custom_bits
|
|
26
|
+
certificate = Certificate.build(bits: 4096)
|
|
27
|
+
assert_equal 4096, certificate.key.n.num_bits
|
|
13
28
|
end
|
|
14
29
|
|
|
15
|
-
def
|
|
16
|
-
|
|
30
|
+
def test_x509_is_x509
|
|
31
|
+
assert_instance_of OpenSSL::X509::Certificate, @certificate.x509
|
|
17
32
|
end
|
|
18
33
|
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
-
assert_equal 4096, certificate.private_key.n.num_bits
|
|
34
|
+
def test_x509_version_is_v3
|
|
35
|
+
assert_equal 2, @certificate.x509.version
|
|
22
36
|
end
|
|
23
37
|
|
|
24
|
-
def
|
|
25
|
-
|
|
38
|
+
def test_x509_has_serial_number
|
|
39
|
+
refute_nil @certificate.x509.serial
|
|
40
|
+
assert @certificate.x509.serial.to_i > 0
|
|
26
41
|
end
|
|
27
42
|
|
|
28
|
-
def
|
|
29
|
-
assert_in_delta
|
|
43
|
+
def test_x509_not_before_is_now
|
|
44
|
+
assert_in_delta @created_at, @certificate.x509.not_before, 1
|
|
30
45
|
end
|
|
31
46
|
|
|
32
|
-
def
|
|
33
|
-
|
|
34
|
-
assert_in_delta
|
|
47
|
+
def test_x509_not_after_is_one_year_from_now
|
|
48
|
+
expected = @created_at + (365 * 24 * 60 * 60)
|
|
49
|
+
assert_in_delta expected, @certificate.x509.not_after, 1
|
|
35
50
|
end
|
|
36
51
|
|
|
37
|
-
def
|
|
38
|
-
assert @certificate.
|
|
52
|
+
def test_x509_is_signed
|
|
53
|
+
assert @certificate.x509.verify(@certificate.key)
|
|
39
54
|
end
|
|
40
55
|
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
def test_sign
|
|
57
|
+
data = "test"
|
|
58
|
+
signature = @certificate.sign(data)
|
|
59
|
+
public_key = @certificate.x509.public_key
|
|
60
|
+
assert public_key.verify("SHA256", signature, data)
|
|
44
61
|
end
|
|
45
62
|
|
|
46
|
-
def
|
|
47
|
-
certificate = Certificate.
|
|
48
|
-
|
|
49
|
-
|
|
63
|
+
def test_x509_with_custom_subject
|
|
64
|
+
certificate = Certificate.build(cn: "Test", o: "Acme")
|
|
65
|
+
subject = certificate.x509.subject.to_s
|
|
66
|
+
assert_equal "/CN=Test/O=Acme", subject
|
|
50
67
|
end
|
|
51
68
|
|
|
52
|
-
def
|
|
69
|
+
def test_x509_with_custom_valid_for
|
|
70
|
+
certificate = Certificate.build(valid_for: 30).x509
|
|
71
|
+
expected = Time.now.utc + (30 * 24 * 60 * 60)
|
|
72
|
+
assert_in_delta expected, certificate.not_after, 1
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def test_x509_with_valid_until
|
|
53
76
|
valid_until = Time.new(2030, 1, 1)
|
|
54
|
-
certificate = Certificate.
|
|
55
|
-
assert_equal valid_until, certificate.
|
|
77
|
+
certificate = Certificate.build(valid_until: valid_until)
|
|
78
|
+
assert_equal valid_until, certificate.x509.not_after
|
|
56
79
|
end
|
|
57
80
|
|
|
58
|
-
def
|
|
59
|
-
assert @certificate.
|
|
81
|
+
def test_key_pem
|
|
82
|
+
assert @certificate.key_pem.start_with?("-----BEGIN")
|
|
60
83
|
end
|
|
61
84
|
|
|
62
|
-
def
|
|
63
|
-
assert @certificate.
|
|
85
|
+
def test_x509_pem
|
|
86
|
+
assert @certificate.x509_pem.start_with?("-----BEGIN")
|
|
64
87
|
end
|
|
65
88
|
|
|
66
89
|
def test_fingerprint
|
|
67
|
-
der = @certificate.
|
|
90
|
+
der = @certificate.x509.to_der
|
|
68
91
|
expected = OpenSSL::Digest::SHA256.hexdigest(der)
|
|
69
92
|
assert_equal expected, @certificate.fingerprint
|
|
70
93
|
end
|
|
71
94
|
|
|
72
95
|
def test_base64
|
|
73
|
-
der = @certificate.
|
|
96
|
+
der = @certificate.x509.to_der
|
|
74
97
|
expected = Base64.strict_encode64(der)
|
|
75
98
|
assert_equal expected, @certificate.base64
|
|
76
99
|
end
|
|
77
100
|
|
|
101
|
+
def test_build_with_block
|
|
102
|
+
certificate = Certificate.build do |c|
|
|
103
|
+
c.cn = "Name"
|
|
104
|
+
c.o = "Org"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
subject = certificate.x509.subject.to_s
|
|
108
|
+
assert_equal "/CN=Name/O=Org", subject
|
|
109
|
+
end
|
|
110
|
+
|
|
78
111
|
def test_load
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
112
|
+
loaded = Certificate.load(
|
|
113
|
+
key_pem: @certificate.key_pem,
|
|
114
|
+
x509_pem: @certificate.x509_pem,
|
|
82
115
|
)
|
|
83
116
|
|
|
84
|
-
assert_equal @certificate.fingerprint,
|
|
117
|
+
assert_equal @certificate.fingerprint, loaded.fingerprint
|
|
85
118
|
end
|
|
86
119
|
end
|
|
87
120
|
end
|
|
@@ -7,5 +7,52 @@ module Lyrebird
|
|
|
7
7
|
def test_issuer
|
|
8
8
|
assert_equal "https://idp.example.com", DEFAULTS.issuer
|
|
9
9
|
end
|
|
10
|
+
|
|
11
|
+
def test_name_id
|
|
12
|
+
assert_equal "user@example.com", DEFAULTS.name_id
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_name_id_format
|
|
16
|
+
assert_equal NAMEID_EMAIL, DEFAULTS.name_id_format
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_recipient
|
|
20
|
+
assert_equal "https://sp.example.com/acs", DEFAULTS.recipient
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_in_response_to
|
|
24
|
+
assert_equal "_request_id", DEFAULTS.in_response_to
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_valid_for
|
|
28
|
+
assert_equal 300, DEFAULTS.valid_for
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_audience
|
|
32
|
+
assert_equal "https://sp.example.com", DEFAULTS.audience
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_authn_context
|
|
36
|
+
expected = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
|
|
37
|
+
assert_equal expected, DEFAULTS.authn_context
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_attributes
|
|
41
|
+
expected = { first_name: "Test", last_name: "User" }
|
|
42
|
+
assert_equal expected, DEFAULTS.attributes
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class ConfigureTest < Minitest::Test
|
|
47
|
+
def test_configure_yields_defaults
|
|
48
|
+
yielded = nil
|
|
49
|
+
Lyrebird.configure { |d| yielded = d }
|
|
50
|
+
assert_same DEFAULTS, yielded
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_configure_freezes_defaults
|
|
54
|
+
Lyrebird.configure { |d| }
|
|
55
|
+
assert DEFAULTS.frozen?
|
|
56
|
+
end
|
|
10
57
|
end
|
|
11
58
|
end
|
|
@@ -4,69 +4,82 @@ require "test_helper"
|
|
|
4
4
|
|
|
5
5
|
module Lyrebird
|
|
6
6
|
class EncryptionTest < Minitest::Test
|
|
7
|
+
NS = {
|
|
8
|
+
"saml" => SAML_ASSERTION_NS,
|
|
9
|
+
"xenc" => XMLENC_NS,
|
|
10
|
+
"ds" => XMLDSIG_NS
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
7
13
|
def setup
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@
|
|
14
|
+
unless defined?(@@certificate)
|
|
15
|
+
@@assertion = Assertion.new.document
|
|
16
|
+
@@element = @@assertion.root
|
|
17
|
+
@@certificate = Certificate.build
|
|
18
|
+
@@encrypted = Encryption.new(@@element, @@certificate).encrypt
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
@assertion = @@assertion
|
|
22
|
+
@element = @@element
|
|
23
|
+
@certificate = @@certificate
|
|
24
|
+
@encrypted = @@encrypted
|
|
25
|
+
@ed = @encrypted.at_xpath("xenc:EncryptedData", NS)
|
|
26
|
+
@ki = @ed.at_xpath("ds:KeyInfo", NS)
|
|
27
|
+
@ek = @ki.at_xpath("xenc:EncryptedKey", NS)
|
|
28
|
+
@cd = @ed.at_xpath("xenc:CipherData", NS)
|
|
16
29
|
end
|
|
17
30
|
|
|
18
31
|
def test_returns_encrypted_assertion_element
|
|
19
32
|
assert_equal "EncryptedAssertion", @encrypted.name
|
|
20
|
-
assert_equal "saml", @encrypted.prefix
|
|
33
|
+
assert_equal "saml", @encrypted.namespace.prefix
|
|
21
34
|
end
|
|
22
35
|
|
|
23
36
|
def test_encrypted_assertion_namespace
|
|
24
|
-
assert_equal SAML_ASSERTION_NS, @encrypted.namespace
|
|
37
|
+
assert_equal SAML_ASSERTION_NS, @encrypted.namespace.href
|
|
25
38
|
end
|
|
26
39
|
|
|
27
40
|
def test_encrypted_data_element
|
|
28
41
|
assert_equal "EncryptedData", @ed.name
|
|
29
|
-
assert_equal "xenc", @ed.prefix
|
|
42
|
+
assert_equal "xenc", @ed.namespace.prefix
|
|
30
43
|
end
|
|
31
44
|
|
|
32
45
|
def test_encrypted_data_namespace
|
|
33
|
-
assert_equal XMLENC_NS, @ed.namespace
|
|
46
|
+
assert_equal XMLENC_NS, @ed.namespace.href
|
|
34
47
|
end
|
|
35
48
|
|
|
36
49
|
def test_encrypted_data_type
|
|
37
|
-
assert_equal "#{XMLENC_NS}Element", @ed
|
|
50
|
+
assert_equal "#{XMLENC_NS}Element", @ed["Type"]
|
|
38
51
|
end
|
|
39
52
|
|
|
40
53
|
def test_encryption_method_element
|
|
41
|
-
em = @ed.
|
|
54
|
+
em = @ed.at_xpath("xenc:EncryptionMethod", NS)
|
|
42
55
|
assert_equal "EncryptionMethod", em.name
|
|
43
|
-
assert_equal "xenc", em.prefix
|
|
56
|
+
assert_equal "xenc", em.namespace.prefix
|
|
44
57
|
end
|
|
45
58
|
|
|
46
59
|
def test_encryption_method_algorithm
|
|
47
|
-
em = @ed.
|
|
48
|
-
assert_equal AES256_CBC, em
|
|
60
|
+
em = @ed.at_xpath("xenc:EncryptionMethod", NS)
|
|
61
|
+
assert_equal AES256_CBC, em["Algorithm"]
|
|
49
62
|
end
|
|
50
63
|
|
|
51
64
|
def test_cipher_data_element
|
|
52
65
|
assert_equal "CipherData", @cd.name
|
|
53
|
-
assert_equal "xenc", @cd.prefix
|
|
66
|
+
assert_equal "xenc", @cd.namespace.prefix
|
|
54
67
|
end
|
|
55
68
|
|
|
56
69
|
def test_cipher_value_element
|
|
57
|
-
cv = @cd.
|
|
70
|
+
cv = @cd.at_xpath("xenc:CipherValue", NS)
|
|
58
71
|
assert_equal "CipherValue", cv.name
|
|
59
|
-
assert_equal "xenc", cv.prefix
|
|
72
|
+
assert_equal "xenc", cv.namespace.prefix
|
|
60
73
|
end
|
|
61
74
|
|
|
62
75
|
def test_cipher_value_is_base64
|
|
63
|
-
cv = @cd.
|
|
76
|
+
cv = @cd.at_xpath("xenc:CipherValue", NS)
|
|
64
77
|
decoded = Base64.strict_decode64(cv.text)
|
|
65
78
|
assert decoded.bytesize > 16
|
|
66
79
|
end
|
|
67
80
|
|
|
68
81
|
def test_cipher_value_starts_with_iv
|
|
69
|
-
cv = @cd.
|
|
82
|
+
cv = @cd.at_xpath("xenc:CipherValue", NS)
|
|
70
83
|
decoded = Base64.strict_decode64(cv.text)
|
|
71
84
|
iv = decoded[0, 16]
|
|
72
85
|
assert_equal 16, iv.bytesize
|
|
@@ -74,50 +87,50 @@ module Lyrebird
|
|
|
74
87
|
|
|
75
88
|
def test_key_info_element
|
|
76
89
|
assert_equal "KeyInfo", @ki.name
|
|
77
|
-
assert_equal "ds", @ki.prefix
|
|
90
|
+
assert_equal "ds", @ki.namespace.prefix
|
|
78
91
|
end
|
|
79
92
|
|
|
80
93
|
def test_key_info_namespace
|
|
81
|
-
assert_equal XMLDSIG_NS, @ki.namespace
|
|
94
|
+
assert_equal XMLDSIG_NS, @ki.namespace.href
|
|
82
95
|
end
|
|
83
96
|
|
|
84
97
|
def test_encrypted_key_element
|
|
85
98
|
assert_equal "EncryptedKey", @ek.name
|
|
86
|
-
assert_equal "xenc", @ek.prefix
|
|
99
|
+
assert_equal "xenc", @ek.namespace.prefix
|
|
87
100
|
end
|
|
88
101
|
|
|
89
102
|
def test_encrypted_key_namespace
|
|
90
|
-
assert_equal XMLENC_NS, @ek.namespace
|
|
103
|
+
assert_equal XMLENC_NS, @ek.namespace.href
|
|
91
104
|
end
|
|
92
105
|
|
|
93
106
|
def test_encrypted_key_encryption_method
|
|
94
|
-
em = @ek.
|
|
107
|
+
em = @ek.at_xpath("xenc:EncryptionMethod", NS)
|
|
95
108
|
assert_equal "EncryptionMethod", em.name
|
|
96
|
-
assert_equal RSA_OAEP, em
|
|
109
|
+
assert_equal RSA_OAEP, em["Algorithm"]
|
|
97
110
|
end
|
|
98
111
|
|
|
99
112
|
def test_encrypted_key_cipher_data
|
|
100
|
-
cd = @ek.
|
|
113
|
+
cd = @ek.at_xpath("xenc:CipherData", NS)
|
|
101
114
|
assert_equal "CipherData", cd.name
|
|
102
|
-
assert_equal "xenc", cd.prefix
|
|
115
|
+
assert_equal "xenc", cd.namespace.prefix
|
|
103
116
|
end
|
|
104
117
|
|
|
105
118
|
def test_encrypted_key_cipher_value
|
|
106
|
-
cv = @ek.
|
|
119
|
+
cv = @ek.at_xpath("xenc:CipherData/xenc:CipherValue", NS)
|
|
107
120
|
assert_equal "CipherValue", cv.name
|
|
108
|
-
assert_equal "xenc", cv.prefix
|
|
121
|
+
assert_equal "xenc", cv.namespace.prefix
|
|
109
122
|
end
|
|
110
123
|
|
|
111
124
|
def test_encrypted_key_cipher_value_is_base64
|
|
112
|
-
cv = @ek.
|
|
125
|
+
cv = @ek.at_xpath("xenc:CipherData/xenc:CipherValue", NS)
|
|
113
126
|
decoded = Base64.strict_decode64(cv.text)
|
|
114
127
|
assert decoded.bytesize > 0
|
|
115
128
|
end
|
|
116
129
|
|
|
117
130
|
def test_encrypted_key_can_be_decrypted
|
|
118
|
-
cv = @ek.
|
|
131
|
+
cv = @ek.at_xpath("xenc:CipherData/xenc:CipherValue", NS)
|
|
119
132
|
encrypted_key = Base64.strict_decode64(cv.text)
|
|
120
|
-
private_key = @certificate.
|
|
133
|
+
private_key = @certificate.key
|
|
121
134
|
padding = OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
|
|
122
135
|
decrypted_key = private_key.private_decrypt(encrypted_key, padding)
|
|
123
136
|
assert_equal 32, decrypted_key.bytesize
|
data/test/lyrebird/id_test.rb
CHANGED
|
@@ -7,5 +7,12 @@ module Lyrebird
|
|
|
7
7
|
def test_generate_starts_with_underscore
|
|
8
8
|
assert ID.generate.start_with?("_")
|
|
9
9
|
end
|
|
10
|
+
|
|
11
|
+
def test_generate_contains_uuid
|
|
12
|
+
id = ID.generate
|
|
13
|
+
uuid = id[1..]
|
|
14
|
+
uuid_pattern = /\A[0-9a-f-]{36}\z/
|
|
15
|
+
assert_match uuid_pattern, uuid
|
|
16
|
+
end
|
|
10
17
|
end
|
|
11
18
|
end
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
require "onelogin/ruby-saml"
|
|
5
|
+
|
|
6
|
+
module Lyrebird
|
|
7
|
+
class IntegrationTest < Minitest::Test
|
|
8
|
+
def setup
|
|
9
|
+
unless defined?(@@idp_cert)
|
|
10
|
+
@@idp_cert = Certificate.build
|
|
11
|
+
@@sp_cert = Certificate.build
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
@idp_cert = @@idp_cert
|
|
15
|
+
@sp_cert = @@sp_cert
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_ruby_saml_parses_signed_response
|
|
19
|
+
name_id = "user@example.com"
|
|
20
|
+
|
|
21
|
+
response = Response.build(sign_with: @idp_cert) do |r|
|
|
22
|
+
r.name_id = name_id
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
settings = OneLogin::RubySaml::Settings.new.tap do |s|
|
|
26
|
+
s.idp_cert = @idp_cert.x509_pem
|
|
27
|
+
s.sp_entity_id = DEFAULTS.audience
|
|
28
|
+
s.assertion_consumer_service_url = DEFAULTS.recipient
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
saml_response = OneLogin::RubySaml::Response.new(
|
|
32
|
+
response.mimic,
|
|
33
|
+
settings: settings
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
assert saml_response.is_valid?, saml_response.errors
|
|
37
|
+
assert_equal name_id, saml_response.nameid
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_ruby_saml_decrypts_signed_and_encrypted_response
|
|
41
|
+
name_id = "user@example.com"
|
|
42
|
+
email = "test@example.com"
|
|
43
|
+
role = "admin"
|
|
44
|
+
|
|
45
|
+
response = Response.build(
|
|
46
|
+
sign_with: @idp_cert,
|
|
47
|
+
encrypt_with: @sp_cert
|
|
48
|
+
) do |r|
|
|
49
|
+
r.name_id = name_id
|
|
50
|
+
|
|
51
|
+
r.attributes do |a|
|
|
52
|
+
a.email = email
|
|
53
|
+
a.role = role
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
settings = OneLogin::RubySaml::Settings.new.tap do |s|
|
|
58
|
+
s.idp_cert = @idp_cert.x509_pem
|
|
59
|
+
s.private_key = @sp_cert.key_pem
|
|
60
|
+
s.sp_entity_id = DEFAULTS.audience
|
|
61
|
+
s.assertion_consumer_service_url = DEFAULTS.recipient
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
saml_response = OneLogin::RubySaml::Response.new(
|
|
65
|
+
response.mimic,
|
|
66
|
+
settings: settings
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
assert saml_response.is_valid?, saml_response.errors
|
|
70
|
+
assert_equal name_id, saml_response.nameid
|
|
71
|
+
assert_equal email, saml_response.attributes["email"]
|
|
72
|
+
assert_equal role, saml_response.attributes["role"]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def test_ruby_saml_parses_idp_initiated_response
|
|
76
|
+
name_id = "user@example.com"
|
|
77
|
+
email = "test@example.com"
|
|
78
|
+
|
|
79
|
+
response = Response.build(sign_with: @idp_cert) do |r|
|
|
80
|
+
r.in_response_to = nil
|
|
81
|
+
r.destination = nil
|
|
82
|
+
r.name_id = name_id
|
|
83
|
+
|
|
84
|
+
r.attributes do |a|
|
|
85
|
+
a.email = email
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
settings = OneLogin::RubySaml::Settings.new.tap do |s|
|
|
90
|
+
s.idp_cert = @idp_cert.x509_pem
|
|
91
|
+
s.sp_entity_id = DEFAULTS.audience
|
|
92
|
+
s.assertion_consumer_service_url = DEFAULTS.recipient
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
saml_response = OneLogin::RubySaml::Response.new(
|
|
96
|
+
response.mimic,
|
|
97
|
+
settings: settings
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
assert saml_response.is_valid?, saml_response.errors
|
|
101
|
+
assert_equal name_id, saml_response.nameid
|
|
102
|
+
assert_equal email, saml_response.attributes["email"]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def test_ruby_saml_parses_multi_value_attributes
|
|
106
|
+
name_id = "user@example.com"
|
|
107
|
+
groups = ["admin", "users", "developers"]
|
|
108
|
+
roles = ["editor", "viewer"]
|
|
109
|
+
|
|
110
|
+
response = Response.build(sign_with: @idp_cert) do |r|
|
|
111
|
+
r.name_id = name_id
|
|
112
|
+
|
|
113
|
+
r.attributes do |a|
|
|
114
|
+
a.groups = groups
|
|
115
|
+
a.roles = roles
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
settings = OneLogin::RubySaml::Settings.new.tap do |s|
|
|
120
|
+
s.idp_cert = @idp_cert.x509_pem
|
|
121
|
+
s.sp_entity_id = DEFAULTS.audience
|
|
122
|
+
s.assertion_consumer_service_url = DEFAULTS.recipient
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
saml_response = OneLogin::RubySaml::Response.new(
|
|
126
|
+
response.mimic,
|
|
127
|
+
settings: settings
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
assert saml_response.is_valid?, saml_response.errors
|
|
131
|
+
assert_equal name_id, saml_response.nameid
|
|
132
|
+
assert_equal groups, saml_response.attributes.multi("groups")
|
|
133
|
+
assert_equal roles, saml_response.attributes.multi("roles")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def test_ruby_saml_validates_custom_validity_period
|
|
137
|
+
name_id = "user@example.com"
|
|
138
|
+
not_before = Time.now.utc - 60
|
|
139
|
+
valid_for = 600
|
|
140
|
+
|
|
141
|
+
response = Response.build(sign_with: @idp_cert) do |r|
|
|
142
|
+
r.name_id = name_id
|
|
143
|
+
r.not_before = not_before
|
|
144
|
+
r.valid_for = valid_for
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
settings = OneLogin::RubySaml::Settings.new.tap do |s|
|
|
148
|
+
s.idp_cert = @idp_cert.x509_pem
|
|
149
|
+
s.sp_entity_id = DEFAULTS.audience
|
|
150
|
+
s.assertion_consumer_service_url = DEFAULTS.recipient
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
saml_response = OneLogin::RubySaml::Response.new(
|
|
154
|
+
response.mimic,
|
|
155
|
+
settings: settings
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
assert saml_response.is_valid?, saml_response.errors
|
|
159
|
+
assert_equal name_id, saml_response.nameid
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def test_ruby_saml_rejects_expired_assertion
|
|
163
|
+
name_id = "user@example.com"
|
|
164
|
+
|
|
165
|
+
response = Response.build(sign_with: @idp_cert) do |r|
|
|
166
|
+
r.name_id = name_id
|
|
167
|
+
r.valid_for = -10
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
settings = OneLogin::RubySaml::Settings.new.tap do |s|
|
|
171
|
+
s.idp_cert = @idp_cert.x509_pem
|
|
172
|
+
s.sp_entity_id = DEFAULTS.audience
|
|
173
|
+
s.assertion_consumer_service_url = DEFAULTS.recipient
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
saml_response = OneLogin::RubySaml::Response.new(
|
|
177
|
+
response.mimic,
|
|
178
|
+
settings: settings
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
assert_operator Time.now.utc, :>=, saml_response.not_on_or_after
|
|
182
|
+
refute saml_response.is_valid?
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def test_ruby_saml_rejects_not_yet_valid_assertion
|
|
186
|
+
name_id = "user@example.com"
|
|
187
|
+
not_before = Time.now.utc + 600
|
|
188
|
+
|
|
189
|
+
response = Response.build(sign_with: @idp_cert) do |r|
|
|
190
|
+
r.name_id = name_id
|
|
191
|
+
r.not_before = not_before
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
settings = OneLogin::RubySaml::Settings.new.tap do |s|
|
|
195
|
+
s.idp_cert = @idp_cert.x509_pem
|
|
196
|
+
s.sp_entity_id = DEFAULTS.audience
|
|
197
|
+
s.assertion_consumer_service_url = DEFAULTS.recipient
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
saml_response = OneLogin::RubySaml::Response.new(
|
|
201
|
+
response.mimic,
|
|
202
|
+
settings: settings
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
assert_operator Time.now.utc, :<, saml_response.not_before
|
|
206
|
+
refute saml_response.is_valid?
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def test_ruby_saml_rejects_tampered_signed_response
|
|
210
|
+
original_email = "user@example.com"
|
|
211
|
+
tampered_email = "attacker@evil.com"
|
|
212
|
+
|
|
213
|
+
response = Response.build(sign_with: @idp_cert) do |r|
|
|
214
|
+
r.name_id = "user@example.com"
|
|
215
|
+
|
|
216
|
+
r.attributes do |a|
|
|
217
|
+
a.email = original_email
|
|
218
|
+
a.role = "user"
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
doc = Nokogiri::XML(Base64.strict_decode64(response.mimic))
|
|
223
|
+
xpath = "//saml:Attribute[@Name='email']/saml:AttributeValue"
|
|
224
|
+
email_attr = doc.at_xpath(xpath, { "saml" => SAML_ASSERTION_NS })
|
|
225
|
+
email_attr.content = tampered_email
|
|
226
|
+
tampered = Base64.strict_encode64(doc.to_xml(save_with: 0))
|
|
227
|
+
|
|
228
|
+
settings = OneLogin::RubySaml::Settings.new.tap do |s|
|
|
229
|
+
s.idp_cert = @idp_cert.x509_pem
|
|
230
|
+
s.sp_entity_id = DEFAULTS.audience
|
|
231
|
+
s.assertion_consumer_service_url = DEFAULTS.recipient
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
saml_response = OneLogin::RubySaml::Response.new(
|
|
235
|
+
tampered,
|
|
236
|
+
settings: settings
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
refute saml_response.is_valid?
|
|
240
|
+
assert_includes saml_response.errors.join(" "), "Signature"
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|