saml_tools 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/LICENSE +24 -0
- data/README.rdoc +65 -0
- data/Rakefile +28 -0
- data/lib/saml_tool.rb +23 -0
- data/lib/saml_tool/certificate.rb +27 -0
- data/lib/saml_tool/decoder.rb +35 -0
- data/lib/saml_tool/encoder.rb +31 -0
- data/lib/saml_tool/erb_builder.rb +33 -0
- data/lib/saml_tool/reader.rb +40 -0
- data/lib/saml_tool/redirect.rb +45 -0
- data/lib/saml_tool/response_reader.rb +148 -0
- data/lib/saml_tool/rsa_key.rb +13 -0
- data/lib/saml_tool/saml.rb +30 -0
- data/lib/saml_tool/settings.rb +24 -0
- data/lib/saml_tool/validator.rb +40 -0
- data/lib/saml_tool/version.rb +8 -0
- data/lib/saml_tools.rb +1 -0
- data/lib/schema/localised-saml-schema-assertion-2.0.xsd +292 -0
- data/lib/schema/localised-saml-schema-protocol-2.0.xsd +309 -0
- data/lib/schema/localised-xenc-schema.xsd +151 -0
- data/lib/schema/xmldsig-core-schema.xsd +318 -0
- data/test/files/TEST_FILES.rdoc +22 -0
- data/test/files/cacert.pem +21 -0
- data/test/files/open_saml_response.xml +56 -0
- data/test/files/request.saml.erb +28 -0
- data/test/files/response.xml +94 -0
- data/test/files/response_template.xml +63 -0
- data/test/files/usercert.p12 +0 -0
- data/test/files/userkey.pem +18 -0
- data/test/files/valid_saml_request.xml +13 -0
- data/test/test_helper.rb +51 -0
- data/test/units/saml_tool/certificate_test.rb +30 -0
- data/test/units/saml_tool/decoder_test.rb +36 -0
- data/test/units/saml_tool/encoder_test.rb +38 -0
- data/test/units/saml_tool/erb_builder_test.rb +50 -0
- data/test/units/saml_tool/reader_test.rb +104 -0
- data/test/units/saml_tool/redirect_test.rb +70 -0
- data/test/units/saml_tool/response_reader_test.rb +144 -0
- data/test/units/saml_tool/rsa_key_test.rb +21 -0
- data/test/units/saml_tool/saml_test.rb +21 -0
- data/test/units/saml_tool/settings_test.rb +36 -0
- data/test/units/saml_tool/validator_test.rb +16 -0
- metadata +168 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative '../../test_helper'
|
2
|
+
|
3
|
+
module SamlTool
|
4
|
+
class EncoderTest < Minitest::Test
|
5
|
+
|
6
|
+
def test_class_encode
|
7
|
+
encoded_saml = Encoder.encode(saml)
|
8
|
+
inflated = inflate Base64.decode64(encoded_saml)
|
9
|
+
assert_equal saml, inflated
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_encode
|
13
|
+
encoded_saml = Encoder.new(saml).encode
|
14
|
+
inflated = inflate Base64.decode64(encoded_saml)
|
15
|
+
assert_equal saml, inflated
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_base64
|
19
|
+
encoded_saml = Encoder.new(saml).base64
|
20
|
+
assert_equal Base64.encode64(saml), encoded_saml
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_zlib
|
24
|
+
encoded_saml = Encoder.new(saml).zlib
|
25
|
+
inflated = inflate encoded_saml
|
26
|
+
assert_equal saml, inflated
|
27
|
+
end
|
28
|
+
|
29
|
+
def inflate(encoded)
|
30
|
+
zstream = Zlib::Inflate.new(-Zlib::MAX_WBITS) # I have no idea why we're using minus Zlib::MAX_WBITS. Zlib documentation suggests just Zlib::MAX_WBITS should work, but it doesn't
|
31
|
+
inflated = zstream.inflate(encoded)
|
32
|
+
zstream.finish
|
33
|
+
zstream.close
|
34
|
+
return inflated
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require_relative '../../test_helper'
|
2
|
+
|
3
|
+
module SamlTool
|
4
|
+
class ErbBuilderTest < Minitest::Test
|
5
|
+
|
6
|
+
def test_build
|
7
|
+
saml = ErbBuilder.build(
|
8
|
+
template: '<foo><%= settings %></foo>',
|
9
|
+
settings: 'bar'
|
10
|
+
)
|
11
|
+
assert_equal '<foo>bar</foo>', saml
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_erb
|
15
|
+
saml = ErbBuilder.new(
|
16
|
+
template: '<foo><%= settings %></foo>',
|
17
|
+
settings: 'bar'
|
18
|
+
)
|
19
|
+
assert_equal '<foo>bar</foo>', saml.to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_erb_can_create_valid_saml
|
23
|
+
saml = ErbBuilder.new(
|
24
|
+
template: request_saml_erb,
|
25
|
+
settings: settings
|
26
|
+
)
|
27
|
+
assert_equal true, Validator.new(saml.to_s).valid?
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
def settings
|
32
|
+
Hashie::Mash.new(
|
33
|
+
id: ('_' + SecureRandom.uuid),
|
34
|
+
issue_instance: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
35
|
+
assertion_consumer_service_url: 'http://localhost:3000/demo',
|
36
|
+
issuer: 'http://localhost:3000',
|
37
|
+
idp_sso_target_url: 'http://localhost:3000/saml/auth',
|
38
|
+
idp_cert_fingerprint: '9E:65:2E:03:06:8D:80:F2:86:C7:6C:77:A1:D9:14:97:0A:4D:F4:4D',
|
39
|
+
name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
|
40
|
+
# Optional for most SAML IdPs
|
41
|
+
authn_context: "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
|
42
|
+
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
def thing
|
47
|
+
'bar'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require_relative '../../test_helper'
|
2
|
+
|
3
|
+
module SamlTool
|
4
|
+
class ReaderTest < Minitest::Test
|
5
|
+
|
6
|
+
def test_reader
|
7
|
+
config = Settings.new(
|
8
|
+
foo: xpath_to_return_foo_text
|
9
|
+
)
|
10
|
+
@reader = Reader.new(nested_saml, config)
|
11
|
+
assert_equal 'bar', @reader.foo
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_reader_with_string_hash_config
|
15
|
+
reader = Reader.new(
|
16
|
+
nested_saml,
|
17
|
+
'foo' => xpath_to_return_foo_text
|
18
|
+
)
|
19
|
+
assert_equal 'bar', reader.foo
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_reader_can_get_attribute
|
23
|
+
reader = Reader.new(
|
24
|
+
nested_saml,
|
25
|
+
'foo' => xpath_to_return_foo_attribute
|
26
|
+
)
|
27
|
+
assert_equal 'that', reader.foo
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_reader_with_name_space
|
31
|
+
reader = Reader.new(
|
32
|
+
response_xml,
|
33
|
+
{foo: '//ds:X509Certificate/text()'},
|
34
|
+
{ds: 'http://www.w3.org/2000/09/xmldsig#'}
|
35
|
+
)
|
36
|
+
assert_equal 'MIIC6D', reader.foo[0...6]
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_value_remembers_source
|
40
|
+
saml = SamlTool::SAML(nested_saml)
|
41
|
+
source = saml.xpath(xpath_to_return_foo_text)
|
42
|
+
test_reader
|
43
|
+
assert_equal source.class, @reader.foo.source.class
|
44
|
+
assert_equal source.to_s, @reader.foo.source.to_s
|
45
|
+
assert_equal @reader.foo, @reader.foo.source.to_s
|
46
|
+
end
|
47
|
+
|
48
|
+
# If nokogiri is passed a namespace of {} it assumes an explicit entry of no namespaces.
|
49
|
+
# Whereas it sees nil namespaces as meaning namesspaces should be ignored.
|
50
|
+
# So nil should be the default behaviour, and can be overridden with {} as required.
|
51
|
+
# This reflects the normal Nokogiri behaviour that is more likely to be the expected
|
52
|
+
# behaviour.
|
53
|
+
def test_default_namespaces
|
54
|
+
reader = Reader.new(nested_saml)
|
55
|
+
assert_equal nil, reader.namespaces
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_to_hash
|
59
|
+
reader = Reader.new(
|
60
|
+
nested_saml,
|
61
|
+
{
|
62
|
+
foo: xpath_to_return_foo_text,
|
63
|
+
this: xpath_to_return_foo_attribute
|
64
|
+
}
|
65
|
+
)
|
66
|
+
expected = {
|
67
|
+
foo: 'bar',
|
68
|
+
this: 'that'
|
69
|
+
}
|
70
|
+
assert_equal expected, reader.to_hash
|
71
|
+
end
|
72
|
+
|
73
|
+
class FooReader < Reader
|
74
|
+
def initialize(saml)
|
75
|
+
super(
|
76
|
+
saml,
|
77
|
+
{
|
78
|
+
foo: 'level_one/foo[1]/text()',
|
79
|
+
this: 'level_one/foo[1]/@this'
|
80
|
+
}
|
81
|
+
)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_reader_via_inherited_class
|
86
|
+
reader = FooReader.new(nested_saml)
|
87
|
+
assert_equal 'bar', reader.foo
|
88
|
+
assert_equal 'that', reader.this
|
89
|
+
end
|
90
|
+
|
91
|
+
def nested_saml
|
92
|
+
'<level_one><foo this="that">bar</foo></level_one>'
|
93
|
+
end
|
94
|
+
|
95
|
+
def xpath_to_return_foo_text
|
96
|
+
'level_one/foo[1]/text()'
|
97
|
+
end
|
98
|
+
|
99
|
+
def xpath_to_return_foo_attribute
|
100
|
+
'level_one/foo[1]/@this'
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require_relative '../../test_helper'
|
2
|
+
|
3
|
+
module SamlTool
|
4
|
+
class RedirectTest < Minitest::Test
|
5
|
+
|
6
|
+
def test_uri
|
7
|
+
redirect = Redirect.uri(
|
8
|
+
to: url,
|
9
|
+
data: {
|
10
|
+
foo: 'bar'
|
11
|
+
}
|
12
|
+
)
|
13
|
+
assert_equal "#{url}?foo=bar", redirect
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_to_s
|
17
|
+
redirect = Redirect.new(
|
18
|
+
to: url,
|
19
|
+
data: {
|
20
|
+
foo: 'bar'
|
21
|
+
}
|
22
|
+
)
|
23
|
+
assert_equal "#{url}?foo=bar", redirect.to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_to_s_with_multiple_data
|
27
|
+
redirect = Redirect.new(
|
28
|
+
to: url,
|
29
|
+
data: {
|
30
|
+
foo: 'bar',
|
31
|
+
this: 'that'
|
32
|
+
}
|
33
|
+
)
|
34
|
+
assert_equal "#{url}?foo=bar&this=that", redirect.to_s
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_to_s_with_existing_parameters
|
38
|
+
redirect = Redirect.new(
|
39
|
+
to: url + '?foo=bar',
|
40
|
+
data: {
|
41
|
+
this: 'that'
|
42
|
+
}
|
43
|
+
)
|
44
|
+
assert_equal "#{url}?foo=bar&this=that", redirect.to_s
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_to_s_with_data_string
|
48
|
+
redirect = Redirect.new(
|
49
|
+
to: url,
|
50
|
+
data: 'foo=bar'
|
51
|
+
)
|
52
|
+
assert_equal "#{url}?foo=bar", redirect.to_s
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_to_s_escapes_data
|
56
|
+
redirect = Redirect.new(
|
57
|
+
to: url,
|
58
|
+
data: {
|
59
|
+
foo: '<bar>'
|
60
|
+
}
|
61
|
+
)
|
62
|
+
assert_equal "#{url}?foo=%3Cbar%3E", redirect.to_s
|
63
|
+
end
|
64
|
+
|
65
|
+
def url
|
66
|
+
'http://example.com/saml/auth'
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require_relative '../../test_helper'
|
2
|
+
|
3
|
+
module SamlTool
|
4
|
+
class ResponseReaderTest < Minitest::Test
|
5
|
+
|
6
|
+
def test_saml
|
7
|
+
assert_kind_of SAML::Document, response_document.saml
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_signatureless
|
11
|
+
assert_kind_of SAML::Document, response_document.signatureless
|
12
|
+
expected = response_document.saml.clone
|
13
|
+
expected.xpath('//ds:Signature', { 'ds' => dsig }).remove
|
14
|
+
assert_equal expected.to_s, response_document.signatureless.to_s
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_signatureless_does_not_impact_saml
|
18
|
+
response_document.signatureless
|
19
|
+
assert response_document.saml.to_s != response_document.signatureless.to_s, 'Changes made in forming signatureless should not also happen to saml'
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_base64_cert
|
23
|
+
base64_cert = response_document_saml.xpath('//ds:X509Certificate/text()', { 'ds' => dsig })
|
24
|
+
assert_equal base64_cert.to_s, response_document.base64_cert
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_certificate
|
28
|
+
assert_kind_of OpenSSL::X509::Certificate, response_document.certificate
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_fingerprint
|
32
|
+
expected = Digest::SHA1.hexdigest(response_document.certificate.to_der)
|
33
|
+
assert_equal expected, response_document.fingerprint
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_canonicalization_method
|
37
|
+
expected = response_document_saml.xpath('//ds:CanonicalizationMethod/@Algorithm', { 'ds' => dsig })
|
38
|
+
assert_equal expected.to_s, response_document.canonicalization_method
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_canonicalization_algorithm
|
42
|
+
expected = Nokogiri::XML::XML_C14N_1_0
|
43
|
+
assert_equal expected, response_document.canonicalization_algorithm
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_reference_uri
|
47
|
+
expected = response_document_saml.xpath('//ds:Reference/@URI', { 'ds' => dsig })
|
48
|
+
assert_equal expected.to_s, response_document.reference_uri
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_inclusive_namespaces
|
52
|
+
assert_equal "", response_document.inclusive_namespaces
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_inclusive_namespaces_when_they_exist_in_saml
|
56
|
+
document = ResponseReader.new(open_saml_request)
|
57
|
+
assert_equal 'xs', document.inclusive_namespaces
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_hashed_element
|
61
|
+
remove_signature_from_assertion
|
62
|
+
assert_equal assertion.to_s, response_document.hashed_element.to_s
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_canonicalized_hashed_element
|
66
|
+
remove_signature_from_assertion
|
67
|
+
expected = assertion.canonicalize(Nokogiri::XML::XML_C14N_1_0, [])
|
68
|
+
assert_equal expected, response_document.canonicalized_hashed_element
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_digest_algorithm
|
72
|
+
assert_equal 'http://www.w3.org/2000/09/xmldsig#sha1', response_document.digest_algorithm
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_digest_algorithm_class
|
76
|
+
assert_equal OpenSSL::Digest::SHA1, response_document.digest_algorithm_class
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_digest_hash
|
80
|
+
expected = OpenSSL::Digest::SHA1.digest(response_document.canonicalized_hashed_element)
|
81
|
+
assert_equal expected, response_document.digest_hash
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_digest_hash_matches_digest_value
|
85
|
+
assert_equal response_document.digest_hash, response_document.decoded_digest_value
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_digests_match?
|
89
|
+
assert_equal true, response_document.digests_match?
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_signature
|
93
|
+
signature_value = response_document_saml.xpath('//ds:SignatureValue', { 'ds' => dsig }).text
|
94
|
+
assert_equal Base64.decode64(signature_value), response_document.signature
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_signature_algorithm_class
|
98
|
+
assert_equal OpenSSL::Digest::SHA1, response_document.signature_algorithm_class
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_canonicalized_signed_info
|
102
|
+
expected = response_document.signed_info.source.first.canonicalize(Nokogiri::XML::XML_C14N_1_0, [])
|
103
|
+
assert_equal expected, response_document.canonicalized_signed_info
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_signature_verified
|
107
|
+
assert_equal true, response_document.signature_verified?
|
108
|
+
end
|
109
|
+
|
110
|
+
def test_structurally_valid
|
111
|
+
assert Validator.new(response_xml).valid?, 'response.xml needs to be valid SAML'
|
112
|
+
assert_equal true, response_document.structurally_valid?
|
113
|
+
end
|
114
|
+
|
115
|
+
def test_valid
|
116
|
+
assert_equal true, response_document.valid?
|
117
|
+
end
|
118
|
+
|
119
|
+
def response_document
|
120
|
+
@response_document ||= ResponseReader.new(response_xml)
|
121
|
+
end
|
122
|
+
|
123
|
+
def assertion
|
124
|
+
@assertion ||= response_document_saml.at_xpath('//saml:Assertion')
|
125
|
+
end
|
126
|
+
|
127
|
+
def remove_signature_from_assertion
|
128
|
+
assertion.xpath('//ds:Signature', { 'ds' => dsig }).remove
|
129
|
+
end
|
130
|
+
|
131
|
+
def response_document_saml
|
132
|
+
@response_document_saml ||= SamlTool::SAML(response_xml)
|
133
|
+
end
|
134
|
+
|
135
|
+
def c14m
|
136
|
+
'http://www.w3.org/2001/10/xml-exc-c14n#'
|
137
|
+
end
|
138
|
+
|
139
|
+
def dsig
|
140
|
+
'http://www.w3.org/2000/09/xmldsig#'
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative '../../test_helper'
|
2
|
+
|
3
|
+
module SamlTool
|
4
|
+
class RsaKeyTest < Minitest::Test
|
5
|
+
|
6
|
+
def test_modulous
|
7
|
+
expected = Base64.encode64(open_ssl_rsa_key.n.to_s(2))
|
8
|
+
assert_equal expected, rsa_key.modulus
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_exponent
|
12
|
+
expected = Base64.encode64(open_ssl_rsa_key.e.to_s(2))
|
13
|
+
assert_equal expected, rsa_key.exponent
|
14
|
+
end
|
15
|
+
|
16
|
+
def rsa_key
|
17
|
+
@rsa_key ||= RsaKey.new(open_ssl_rsa_key)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module SamlTool
|
4
|
+
class SamlTest < Minitest::Test
|
5
|
+
|
6
|
+
def test_document
|
7
|
+
document = SamlTool::SAML(valid_xml)
|
8
|
+
assert_kind_of Nokogiri::XML::Document, document
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_parse
|
12
|
+
document = SamlTool::SAML.parse valid_xml
|
13
|
+
assert_kind_of Nokogiri::XML::Document, document
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_document_parse
|
17
|
+
document = SamlTool::SAML::Document.parse valid_xml
|
18
|
+
assert_kind_of Nokogiri::XML::Document, document
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|