samlr 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of samlr might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +8 -0
- data/LICENSE +176 -0
- data/README.md +182 -0
- data/Rakefile +12 -0
- data/bin/samlr +46 -0
- data/config/schemas/XMLSchema.xsd +2534 -0
- data/config/schemas/saml-schema-assertion-2.0.xsd +283 -0
- data/config/schemas/saml-schema-metadata-2.0.xsd +337 -0
- data/config/schemas/saml-schema-protocol-2.0.xsd +302 -0
- data/config/schemas/xenc-schema.xsd +146 -0
- data/config/schemas/xml.xsd +287 -0
- data/config/schemas/xmldsig-core-schema.xsd +318 -0
- data/lib/samlr.rb +52 -0
- data/lib/samlr/assertion.rb +91 -0
- data/lib/samlr/certificate.rb +23 -0
- data/lib/samlr/command.rb +41 -0
- data/lib/samlr/condition.rb +31 -0
- data/lib/samlr/errors.rb +22 -0
- data/lib/samlr/fingerprint.rb +44 -0
- data/lib/samlr/logout_request.rb +7 -0
- data/lib/samlr/reference.rb +32 -0
- data/lib/samlr/request.rb +37 -0
- data/lib/samlr/response.rb +68 -0
- data/lib/samlr/signature.rb +129 -0
- data/lib/samlr/tools.rb +108 -0
- data/lib/samlr/tools/certificate_builder.rb +74 -0
- data/lib/samlr/tools/logout_request_builder.rb +27 -0
- data/lib/samlr/tools/metadata_builder.rb +41 -0
- data/lib/samlr/tools/request_builder.rb +44 -0
- data/lib/samlr/tools/response_builder.rb +157 -0
- data/lib/samlr/tools/timestamp.rb +26 -0
- data/samlr.gemspec +19 -0
- data/test/fixtures/default_samlr_certificate.pem +11 -0
- data/test/fixtures/default_samlr_private_key.pem +9 -0
- data/test/fixtures/no_cert_response.xml +2 -0
- data/test/fixtures/sample_metadata.xml +7 -0
- data/test/fixtures/sample_response.xml +2 -0
- data/test/test_helper.rb +55 -0
- data/test/unit/test_assertion.rb +54 -0
- data/test/unit/test_condition.rb +71 -0
- data/test/unit/test_fingerprint.rb +45 -0
- data/test/unit/test_logout_request.rb +39 -0
- data/test/unit/test_reference.rb +32 -0
- data/test/unit/test_request.rb +34 -0
- data/test/unit/test_response.rb +94 -0
- data/test/unit/test_response_scenarios.rb +111 -0
- data/test/unit/test_signature.rb +54 -0
- data/test/unit/test_timestamp.rb +58 -0
- data/test/unit/test_tools.rb +100 -0
- data/test/unit/tools/test_certificate_builder.rb +41 -0
- data/test/unit/tools/test_logout_request_builder.rb +26 -0
- data/test/unit/tools/test_metadata_builder.rb +26 -0
- data/test/unit/tools/test_request_builder.rb +35 -0
- data/test/unit/tools/test_response_builder.rb +19 -0
- metadata +184 -0
@@ -0,0 +1,111 @@
|
|
1
|
+
require File.expand_path("test/test_helper")
|
2
|
+
|
3
|
+
# The tests in here are integraton level tests. They pass various mutations of a response
|
4
|
+
# document to the stack and asserts behavior.
|
5
|
+
describe Samlr do
|
6
|
+
|
7
|
+
describe "a valid response" do
|
8
|
+
subject { saml_response(:certificate => TEST_CERTIFICATE) }
|
9
|
+
|
10
|
+
it "verifies" do
|
11
|
+
assert subject.verify!
|
12
|
+
assert_equal "someone@example.org", subject.name_id
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "an invalid fingerprint" do
|
17
|
+
subject { saml_response(:certificate => TEST_CERTIFICATE, :fingerprint => "hello") }
|
18
|
+
it "fails" do
|
19
|
+
assert_raises(Samlr::FingerprintError) { subject.verify! }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "an unsatisfied before condition" do
|
24
|
+
subject { saml_response(:certificate => TEST_CERTIFICATE, :not_before => Samlr::Tools::Timestamp.stamp(Time.now + 60)) }
|
25
|
+
|
26
|
+
it "fails" do
|
27
|
+
assert_raises(Samlr::ConditionsError) { subject.verify! }
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "when jitter is in effect" do
|
31
|
+
after { Samlr.jitter = nil }
|
32
|
+
|
33
|
+
it "passes" do
|
34
|
+
Samlr.jitter = 500
|
35
|
+
assert subject.verify!
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "an unsatisfied after condition" do
|
41
|
+
subject { saml_response(:certificate => TEST_CERTIFICATE, :not_on_or_after => Samlr::Tools::Timestamp.stamp(Time.now - 60)) }
|
42
|
+
|
43
|
+
it "fails" do
|
44
|
+
assert_raises(Samlr::ConditionsError) { subject.verify! }
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "when jitter is in effect" do
|
48
|
+
after { Samlr.jitter = nil }
|
49
|
+
|
50
|
+
it "passes" do
|
51
|
+
Samlr.jitter = 500
|
52
|
+
assert subject.verify!
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "when there are no attributes" do
|
58
|
+
subject { saml_response(:certificate => TEST_CERTIFICATE, :attributes => {}) }
|
59
|
+
|
60
|
+
it "returns an empty hash" do
|
61
|
+
assert_equal({}, subject.attributes)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "when there are no signatures" do
|
66
|
+
subject { saml_response(:certificate => TEST_CERTIFICATE, :sign_assertion => false, :sign_response => false) }
|
67
|
+
|
68
|
+
it "fails" do
|
69
|
+
assert_raises(Samlr::SignatureError) { subject.verify! }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "when there is no keyinfo" do
|
74
|
+
subject { saml_response(:certificate => TEST_CERTIFICATE, :skip_keyinfo => true) }
|
75
|
+
|
76
|
+
it "fails" do
|
77
|
+
assert_raises(Samlr::SignatureError) { subject.verify! }
|
78
|
+
end
|
79
|
+
|
80
|
+
describe "when a matching external cert is provided" do
|
81
|
+
it "passes" do
|
82
|
+
subject.options[:certificate] = TEST_CERTIFICATE.x509
|
83
|
+
assert subject.verify!
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe "when a non-matching external cert is provided" do
|
88
|
+
it "fails" do
|
89
|
+
subject.options[:certificate] = Samlr::Tools::CertificateBuilder.new.x509
|
90
|
+
assert_raises(Samlr::FingerprintError) { subject.verify! }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe "when there's no assertion" do
|
96
|
+
subject { saml_response(:certificate => TEST_CERTIFICATE, :sign_assertion => false, :skip_assertion => true) }
|
97
|
+
|
98
|
+
it "fails" do
|
99
|
+
assert_raises(Samlr::FormatError) { subject.verify! }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "duplicate element ids" do
|
104
|
+
subject { saml_response(:certificate => TEST_CERTIFICATE, :response_id => "abcdef", :assertion_id => "abcdef") }
|
105
|
+
|
106
|
+
it "fails" do
|
107
|
+
assert_raises(Samlr::FormatError) { subject.verify! }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require File.expand_path("test/test_helper")
|
2
|
+
require "openssl"
|
3
|
+
|
4
|
+
describe Samlr::Signature do
|
5
|
+
before do
|
6
|
+
@response = fixed_saml_response
|
7
|
+
@signature = @response.signature
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "#signature_algorithm" do
|
11
|
+
it "should defer to Samlr::Tools::algorithm" do
|
12
|
+
Samlr::Tools.stub(:algorithm, "hello") do
|
13
|
+
assert_match "hello", @signature.send(:signature_method)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "#references" do
|
19
|
+
it "should extract the reference to the signed document" do
|
20
|
+
assert_equal @response.document.children.first, @response.document.at(".//*[@ID='#{@signature.send(:references).first.uri}']")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#certificate" do
|
25
|
+
it "should extract the certificate" do
|
26
|
+
assert_equal TEST_CERTIFICATE.to_certificate, @signature.send(:certificate)
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "when there is no X509 certificate" do
|
30
|
+
it "should raise a signature error" do
|
31
|
+
@signature.stub(:certificate_node, nil) do
|
32
|
+
assert_raises(Samlr::SignatureError) { @signature.send(:certificate) }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "#verify_digests!" do
|
39
|
+
describe "when there are duplicate element ids" do
|
40
|
+
before do
|
41
|
+
@signature.document.at("/samlp:Response/saml:Assertion")["ID"] = @signature.document.root["ID"]
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should raise" do
|
45
|
+
begin
|
46
|
+
@signature.send(:verify_digests!)
|
47
|
+
flunk("Excepted to raise due to duplicate elements")
|
48
|
+
rescue Samlr::SignatureError => e
|
49
|
+
assert_equal "Reference validation error: Invalid element references", e.message
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require File.expand_path("test/test_helper")
|
2
|
+
|
3
|
+
describe Samlr::Tools::Timestamp do
|
4
|
+
before { Samlr.jitter = nil }
|
5
|
+
after { Samlr.jitter = nil }
|
6
|
+
|
7
|
+
describe "::parse" do
|
8
|
+
before { @time = ::Time.now }
|
9
|
+
it "turns an iso8601 string into a time instance" do
|
10
|
+
iso8601 = @time.utc.iso8601
|
11
|
+
assert_equal @time.to_i, Samlr::Tools::Timestamp.parse(iso8601).to_i
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "::stamp" do
|
16
|
+
it "converts a given time to an iso8601 string in UTC" do
|
17
|
+
assert_equal "2012-08-08T18:28:38Z", Samlr::Tools::Timestamp.stamp(Time.at(1344450518))
|
18
|
+
end
|
19
|
+
|
20
|
+
it "defaults to a current timestamp in iso8601" do
|
21
|
+
assert ::Time.iso8601(Samlr::Tools::Timestamp.stamp).is_a?(Time)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "::not_on_or_after?" do
|
26
|
+
describe "when no jitter is allowed" do
|
27
|
+
it "disallows imprecision" do
|
28
|
+
assert Samlr::Tools::Timestamp.not_on_or_after?(Time.now + 5)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "when jitter is allowed" do
|
33
|
+
before { Samlr.jitter = 10 }
|
34
|
+
|
35
|
+
it "allows imprecision" do
|
36
|
+
assert Samlr::Tools::Timestamp.not_on_or_after?(Time.now - 5)
|
37
|
+
refute Samlr::Tools::Timestamp.not_on_or_after?(Time.now - 15)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "::before?" do
|
43
|
+
describe "when no jitter is allowed" do
|
44
|
+
it "disallows imprecision" do
|
45
|
+
assert Samlr::Tools::Timestamp.not_before?(Time.now - 5)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "when jitter is allowed" do
|
50
|
+
before { Samlr.jitter = 10 }
|
51
|
+
|
52
|
+
it "allows imprecision" do
|
53
|
+
assert Samlr::Tools::Timestamp.not_before?(Time.now + 5)
|
54
|
+
refute Samlr::Tools::Timestamp.not_before?(Time.now + 15)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require File.expand_path("test/test_helper")
|
2
|
+
require "openssl"
|
3
|
+
|
4
|
+
describe Samlr::Tools do
|
5
|
+
|
6
|
+
describe "::canonicalize" do
|
7
|
+
before do
|
8
|
+
@fixture = fixed_saml_response.document.to_xml
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should namespace the SignedInfo element" do
|
12
|
+
path = "/samlp:Response/ds:Signature/ds:SignedInfo"
|
13
|
+
assert_match '<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">', Samlr::Tools.canonicalize(@fixture, { :path => path })
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "::uuid" do
|
18
|
+
it "generates a valid xs:ID" do
|
19
|
+
assert Samlr::Tools.uuid !~ /^\d/
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "::algorithm" do
|
24
|
+
[ 1, 384, 512 ].each do |i|
|
25
|
+
describe "when fed SHA#{i}" do
|
26
|
+
subject { "#sha#{i}" }
|
27
|
+
|
28
|
+
it "should return the corresponding implementation" do
|
29
|
+
assert_equal eval("OpenSSL::Digest::SHA#{i}"), Samlr::Tools.algorithm(subject)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "when not specified" do
|
35
|
+
subject { nil }
|
36
|
+
|
37
|
+
it "should default to SHA1" do
|
38
|
+
assert_equal OpenSSL::Digest::SHA1, Samlr::Tools.algorithm(subject)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "when not known" do
|
43
|
+
subject { "sha73" }
|
44
|
+
|
45
|
+
it "should default to SHA1" do
|
46
|
+
assert_equal OpenSSL::Digest::SHA1, Samlr::Tools.algorithm(subject)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "::encode and ::decode" do
|
52
|
+
it "compresses a string in a reversible fashion" do
|
53
|
+
assert_equal "12345678", Samlr::Tools.decode(Samlr::Tools.encode("12345678"))
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "::validate" do
|
58
|
+
subject { saml_response_document(:certificate => TEST_CERTIFICATE) }
|
59
|
+
|
60
|
+
it "returns true for valid documents" do
|
61
|
+
assert Samlr::Tools.validate(:document => subject)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "returns false for invalid documents" do
|
65
|
+
mangled = subject.gsub("Assertion", "AyCaramba")
|
66
|
+
refute Samlr::Tools.validate(:document => mangled)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "does not change the working directory" do
|
70
|
+
path = Dir.pwd
|
71
|
+
assert Samlr::Tools.validate(:document => subject)
|
72
|
+
assert_equal path, Dir.pwd
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "::validate!" do
|
77
|
+
subject { saml_response_document(:certificate => TEST_CERTIFICATE) }
|
78
|
+
|
79
|
+
it "returns true for valid documents" do
|
80
|
+
assert Samlr::Tools.validate!(:document => subject)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "raises for invalid documents" do
|
84
|
+
mangled = subject.gsub("Assertion", "AyCaramba")
|
85
|
+
|
86
|
+
begin
|
87
|
+
Samlr::Tools.validate!(:document => mangled)
|
88
|
+
flunk "Errors expected"
|
89
|
+
rescue Samlr::FormatError => e
|
90
|
+
assert_equal "Schema validation failed", e.message
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
it "does not change the working directory" do
|
95
|
+
path = Dir.pwd
|
96
|
+
assert Samlr::Tools.validate!(:document => subject)
|
97
|
+
assert_equal path, Dir.pwd
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require File.expand_path("test/test_helper")
|
2
|
+
|
3
|
+
describe Samlr::Tools::CertificateBuilder do
|
4
|
+
before { @certificate = TEST_CERTIFICATE }
|
5
|
+
|
6
|
+
it "provides a certificate" do
|
7
|
+
assert_equal OpenSSL::X509::Certificate, @certificate.x509.class
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "#verify" do
|
11
|
+
it "verifies its own signature" do
|
12
|
+
assert @certificate.verify(@certificate.sign("12345678"), "12345678")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "serialization" do
|
17
|
+
before do
|
18
|
+
@path = Dir.tmpdir
|
19
|
+
Dir.glob("#{@path}/*.pem").map { |f| File.unlink(f) }
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "self#dump" do
|
23
|
+
before { Samlr::Tools::CertificateBuilder.dump(@path, @certificate) }
|
24
|
+
|
25
|
+
it "creates a key file and a certificate file on disk" do
|
26
|
+
state = Dir.glob("#{@path}/*.pem")
|
27
|
+
assert_equal 2, state.size
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "#load" do
|
31
|
+
before { @loaded = Samlr::Tools::CertificateBuilder.load(@path) }
|
32
|
+
|
33
|
+
it "verified the signature signed by the unserialized certificate" do
|
34
|
+
assert @loaded.verify(@certificate.sign("12345678"), "12345678")
|
35
|
+
assert @certificate.verify(@loaded.sign("12345678"), "12345678")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require File.expand_path("test/test_helper")
|
2
|
+
|
3
|
+
describe Samlr::Tools::LogoutRequestBuilder do
|
4
|
+
describe "#build" do
|
5
|
+
before do
|
6
|
+
@xml = Samlr::Tools::LogoutRequestBuilder.build(
|
7
|
+
:issuer => "https://sp.example.com/saml2",
|
8
|
+
:name_id => "test@test.com"
|
9
|
+
)
|
10
|
+
|
11
|
+
@doc = Nokogiri::XML(@xml) { |c| c.strict }
|
12
|
+
end
|
13
|
+
|
14
|
+
it "generates a request document" do
|
15
|
+
assert_equal "LogoutRequest", @doc.root.name
|
16
|
+
|
17
|
+
issuer = @doc.root.at("./saml:Issuer", Samlr::NS_MAP)
|
18
|
+
assert_equal "https://sp.example.com/saml2", issuer.text
|
19
|
+
end
|
20
|
+
|
21
|
+
it "validates against schemas" do
|
22
|
+
result = Samlr::Tools.validate(:document => @xml)
|
23
|
+
assert result
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require File.expand_path("test/test_helper")
|
2
|
+
|
3
|
+
describe Samlr::Tools::MetadataBuilder do
|
4
|
+
describe "#build" do
|
5
|
+
before do
|
6
|
+
@xml = Samlr::Tools::MetadataBuilder.build({
|
7
|
+
:entity_id => "https://sp.example.com/saml2",
|
8
|
+
:name_identity_format => "identity_format",
|
9
|
+
:consumer_service_url => "https://support.sp.example.com/"
|
10
|
+
})
|
11
|
+
|
12
|
+
@doc = Nokogiri::XML(@xml) { |c| c.strict }
|
13
|
+
end
|
14
|
+
|
15
|
+
it "generates a metadata document" do
|
16
|
+
assert_equal "EntityDescriptor", @doc.root.name
|
17
|
+
assert_equal "identity_format", @doc.at("//md:NameIDFormat", { "md" => Samlr::NS_MAP["md"] }).text
|
18
|
+
end
|
19
|
+
|
20
|
+
it "validates against schemas" do
|
21
|
+
result = Samlr::Tools.validate(:document => @xml, :schema => Samlr::META_SCHEMA)
|
22
|
+
assert result
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require File.expand_path("test/test_helper")
|
2
|
+
|
3
|
+
describe Samlr::Tools::RequestBuilder do
|
4
|
+
describe "#build" do
|
5
|
+
before do
|
6
|
+
@xml = Samlr::Tools::RequestBuilder.build({
|
7
|
+
:issuer => "https://sp.example.com/saml2",
|
8
|
+
:name_identity_format => "identity_format",
|
9
|
+
:allow_create => "true",
|
10
|
+
:consumer_service_url => "https://support.sp.example.com/",
|
11
|
+
:authn_context => "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
|
12
|
+
})
|
13
|
+
|
14
|
+
@doc = Nokogiri::XML(@xml) { |c| c.strict }
|
15
|
+
end
|
16
|
+
|
17
|
+
it "generates a request document" do
|
18
|
+
assert_equal "AuthnRequest", @doc.root.name
|
19
|
+
assert_equal "https://support.sp.example.com/", @doc.root["AssertionConsumerServiceURL"]
|
20
|
+
|
21
|
+
issuer = @doc.root.at("./saml:Issuer", Samlr::NS_MAP)
|
22
|
+
assert_equal "https://sp.example.com/saml2", issuer.text
|
23
|
+
|
24
|
+
name_id_policy = @doc.root.at("./samlp:NameIDPolicy", Samlr::NS_MAP)
|
25
|
+
assert_equal "true", name_id_policy["AllowCreate"]
|
26
|
+
assert_equal "identity_format", name_id_policy["Format"]
|
27
|
+
end
|
28
|
+
|
29
|
+
it "validates against schemas" do
|
30
|
+
result = Samlr::Tools.validate(:document => @xml)
|
31
|
+
assert result
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|