saml_idp 0.7.2 → 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 +5 -5
- data/Gemfile +1 -1
- data/README.md +71 -55
- data/lib/saml_idp/assertion_builder.rb +28 -3
- data/lib/saml_idp/configurator.rb +9 -3
- data/lib/saml_idp/controller.rb +27 -16
- data/lib/saml_idp/encryptor.rb +0 -1
- data/lib/saml_idp/fingerprint.rb +19 -0
- data/lib/saml_idp/incoming_metadata.rb +31 -1
- data/lib/saml_idp/metadata_builder.rb +25 -9
- data/lib/saml_idp/persisted_metadata.rb +4 -0
- data/lib/saml_idp/request.rb +103 -13
- data/lib/saml_idp/response_builder.rb +26 -6
- data/lib/saml_idp/saml_response.rb +62 -28
- data/lib/saml_idp/service_provider.rb +16 -6
- data/lib/saml_idp/signable.rb +1 -2
- data/lib/saml_idp/signature_builder.rb +2 -1
- data/lib/saml_idp/signed_info_builder.rb +2 -2
- data/lib/saml_idp/version.rb +1 -1
- data/lib/saml_idp/xml_security.rb +20 -15
- data/lib/saml_idp.rb +4 -3
- data/saml_idp.gemspec +46 -42
- data/spec/acceptance/idp_controller_spec.rb +5 -4
- data/spec/lib/saml_idp/algorithmable_spec.rb +6 -6
- data/spec/lib/saml_idp/assertion_builder_spec.rb +151 -8
- data/spec/lib/saml_idp/attribute_decorator_spec.rb +8 -8
- data/spec/lib/saml_idp/configurator_spec.rb +45 -7
- data/spec/lib/saml_idp/controller_spec.rb +86 -25
- data/spec/lib/saml_idp/encryptor_spec.rb +4 -4
- data/spec/lib/saml_idp/fingerprint_spec.rb +14 -0
- data/spec/lib/saml_idp/incoming_metadata_spec.rb +134 -0
- data/spec/lib/saml_idp/metadata_builder_spec.rb +30 -17
- data/spec/lib/saml_idp/name_id_formatter_spec.rb +3 -3
- data/spec/lib/saml_idp/request_spec.rb +153 -64
- data/spec/lib/saml_idp/response_builder_spec.rb +5 -3
- data/spec/lib/saml_idp/saml_response_spec.rb +146 -12
- data/spec/lib/saml_idp/service_provider_spec.rb +2 -2
- data/spec/lib/saml_idp/signable_spec.rb +1 -1
- data/spec/lib/saml_idp/signature_builder_spec.rb +2 -2
- data/spec/lib/saml_idp/signed_info_builder_spec.rb +3 -3
- data/spec/rails_app/app/controllers/saml_controller.rb +1 -1
- data/spec/rails_app/app/controllers/saml_idp_controller.rb +55 -3
- data/{app → spec/rails_app/app}/views/saml_idp/idp/new.html.erb +3 -4
- data/{app → spec/rails_app/app}/views/saml_idp/idp/saml_post.html.erb +1 -1
- data/spec/rails_app/config/application.rb +1 -6
- data/spec/rails_app/config/boot.rb +1 -1
- data/spec/rails_app/config/environments/development.rb +2 -5
- data/spec/rails_app/config/environments/production.rb +1 -0
- data/spec/rails_app/config/environments/test.rb +1 -0
- data/spec/spec_helper.rb +23 -1
- data/spec/support/certificates/sp_cert_req.csr +12 -0
- data/spec/support/certificates/sp_private_key.pem +16 -0
- data/spec/support/certificates/sp_x509_cert.crt +18 -0
- data/spec/support/saml_request_macros.rb +107 -5
- data/spec/support/security_helpers.rb +12 -2
- data/spec/xml_security_spec.rb +19 -15
- metadata +146 -80
- data/app/controllers/saml_idp/idp_controller.rb +0 -59
- data/spec/lib/saml_idp/.assertion_builder_spec.rb.swp +0 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module SamlIdp
|
|
4
|
+
|
|
5
|
+
metadata_1 = <<-eos
|
|
6
|
+
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="test" entityID="https://test-saml.com/saml">
|
|
7
|
+
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol" AuthnRequestsSigned="false" WantAssertionsSigned="false">
|
|
8
|
+
</md:SPSSODescriptor>
|
|
9
|
+
</md:EntityDescriptor>
|
|
10
|
+
eos
|
|
11
|
+
|
|
12
|
+
metadata_2 = <<-eos
|
|
13
|
+
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="test" entityID="https://test-saml.com/saml">
|
|
14
|
+
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol" AuthnRequestsSigned="true" WantAssertionsSigned="true">
|
|
15
|
+
</md:SPSSODescriptor>
|
|
16
|
+
</md:EntityDescriptor>
|
|
17
|
+
eos
|
|
18
|
+
|
|
19
|
+
metadata_3 = <<-eos
|
|
20
|
+
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="test" entityID="https://test-saml.com/saml">
|
|
21
|
+
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol" AuthnRequestsSigned="true">
|
|
22
|
+
</md:SPSSODescriptor>
|
|
23
|
+
</md:EntityDescriptor>
|
|
24
|
+
eos
|
|
25
|
+
|
|
26
|
+
metadata_4 = <<-eos
|
|
27
|
+
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="test" entityID="https://test-saml.com/saml">
|
|
28
|
+
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
|
29
|
+
</md:SPSSODescriptor>
|
|
30
|
+
</md:EntityDescriptor>
|
|
31
|
+
eos
|
|
32
|
+
|
|
33
|
+
metadata_5 = <<-eos
|
|
34
|
+
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="test" entityID="https://test-saml.com/saml">
|
|
35
|
+
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
|
36
|
+
<md:KeyDescriptor>
|
|
37
|
+
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
|
38
|
+
<ds:X509Data>
|
|
39
|
+
<ds:X509Certificate>MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnht3GR...</ds:X509Certificate>
|
|
40
|
+
</ds:X509Data>
|
|
41
|
+
</ds:KeyInfo>
|
|
42
|
+
</md:KeyDescriptor>
|
|
43
|
+
</md:SPSSODescriptor>
|
|
44
|
+
</md:EntityDescriptor>
|
|
45
|
+
eos
|
|
46
|
+
|
|
47
|
+
metadata_6 = <<-eos
|
|
48
|
+
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="test" entityID="https://test-saml.com/saml">
|
|
49
|
+
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
|
50
|
+
<md:KeyDescriptor use="signing">
|
|
51
|
+
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
|
52
|
+
<ds:X509Data>
|
|
53
|
+
<ds:X509Certificate>MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmw6vGr...</ds:X509Certificate>
|
|
54
|
+
</ds:X509Data>
|
|
55
|
+
</ds:KeyInfo>
|
|
56
|
+
</md:KeyDescriptor>
|
|
57
|
+
</md:SPSSODescriptor>
|
|
58
|
+
</md:EntityDescriptor>
|
|
59
|
+
eos
|
|
60
|
+
|
|
61
|
+
metadata_7 = <<-eos
|
|
62
|
+
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="test" entityID="https://test-saml.com/saml">
|
|
63
|
+
<md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
|
64
|
+
<md:KeyDescriptor use="encryption">
|
|
65
|
+
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
|
66
|
+
<ds:X509Data>
|
|
67
|
+
<ds:X509Certificate>MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1dX3Gr...</ds:X509Certificate>
|
|
68
|
+
</ds:X509Data>
|
|
69
|
+
</ds:KeyInfo>
|
|
70
|
+
</md:KeyDescriptor>
|
|
71
|
+
</md:SPSSODescriptor>
|
|
72
|
+
</md:EntityDescriptor>
|
|
73
|
+
eos
|
|
74
|
+
|
|
75
|
+
describe IncomingMetadata do
|
|
76
|
+
it 'should properly set sign_assertions to false' do
|
|
77
|
+
metadata = SamlIdp::IncomingMetadata.new(metadata_1)
|
|
78
|
+
expect(metadata.sign_assertions).to eq(false)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'should properly set entity_id as https://test-saml.com/saml' do
|
|
82
|
+
metadata = SamlIdp::IncomingMetadata.new(metadata_1)
|
|
83
|
+
expect(metadata.entity_id).to eq('https://test-saml.com/saml')
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'should properly set sign_assertions to true' do
|
|
87
|
+
metadata = SamlIdp::IncomingMetadata.new(metadata_2)
|
|
88
|
+
expect(metadata.sign_assertions).to eq(true)
|
|
89
|
+
expect(metadata.sign_authn_request).to eq(true)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'should properly set sign_assertions to false when WantAssertionsSigned is not included' do
|
|
93
|
+
metadata = SamlIdp::IncomingMetadata.new(metadata_3)
|
|
94
|
+
expect(metadata.sign_assertions).to eq(false)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'should properly set sign_authn_request to false when AuthnRequestsSigned is not included' do
|
|
98
|
+
metadata = SamlIdp::IncomingMetadata.new(metadata_4)
|
|
99
|
+
expect(metadata.sign_authn_request).to eq(false)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'should properly set unspecified_certificate when present' do
|
|
103
|
+
metadata = SamlIdp::IncomingMetadata.new(metadata_5)
|
|
104
|
+
expect(metadata.unspecified_certificate).to eq('MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnht3GR...')
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'should return empty unspecified_certificate when not present' do
|
|
108
|
+
metadata = SamlIdp::IncomingMetadata.new(metadata_1)
|
|
109
|
+
expect(metadata.unspecified_certificate).to eq('')
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'should properly set signing_certificate when present but not unspecified_certificate' do
|
|
113
|
+
metadata = SamlIdp::IncomingMetadata.new(metadata_6)
|
|
114
|
+
expect(metadata.signing_certificate).to eq('MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmw6vGr...')
|
|
115
|
+
expect(metadata.unspecified_certificate).to eq('')
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'should return empty signing_certificate when not present' do
|
|
119
|
+
metadata = SamlIdp::IncomingMetadata.new(metadata_1)
|
|
120
|
+
expect(metadata.signing_certificate).to eq('')
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'should properly set encryption_certificate when present but not unspecified_certificate' do
|
|
124
|
+
metadata = SamlIdp::IncomingMetadata.new(metadata_7)
|
|
125
|
+
expect(metadata.encryption_certificate).to eq('MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1dX3Gr...')
|
|
126
|
+
expect(metadata.unspecified_certificate).to eq('')
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'should return empty encryption_certificate when not present' do
|
|
130
|
+
metadata = SamlIdp::IncomingMetadata.new(metadata_1)
|
|
131
|
+
expect(metadata.encryption_certificate).to eq('')
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -2,18 +2,39 @@ require 'spec_helper'
|
|
|
2
2
|
module SamlIdp
|
|
3
3
|
describe MetadataBuilder do
|
|
4
4
|
it "has a valid fresh" do
|
|
5
|
-
subject.fresh.
|
|
5
|
+
expect(subject.fresh).to_not be_empty
|
|
6
6
|
end
|
|
7
7
|
|
|
8
8
|
it "signs valid xml" do
|
|
9
|
-
Saml::XML::Document.parse(subject.signed).valid_signature?(Default::FINGERPRINT).
|
|
9
|
+
expect(Saml::XML::Document.parse(subject.signed).valid_signature?("", Default::FINGERPRINT)).to be_truthy
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
it "includes logout element" do
|
|
13
13
|
subject.configurator.single_logout_service_post_location = 'https://example.com/saml/logout'
|
|
14
|
-
subject.
|
|
15
|
-
|
|
16
|
-
)
|
|
14
|
+
subject.configurator.single_logout_service_redirect_location = 'https://example.com/saml/logout'
|
|
15
|
+
expect(subject.fresh).to match('<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://example.com/saml/logout"/>')
|
|
16
|
+
expect(subject.fresh).to match('<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://example.com/saml/logout"/>')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'will not includes empty logout endpoint' do
|
|
20
|
+
subject.configurator.single_logout_service_post_location = ''
|
|
21
|
+
subject.configurator.single_logout_service_redirect_location = nil
|
|
22
|
+
expect(subject.fresh).not_to match('<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"')
|
|
23
|
+
expect(subject.fresh).not_to match('<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'will includes sso element' do
|
|
27
|
+
subject.configurator.single_service_post_location = 'https://example.com/saml/sso'
|
|
28
|
+
subject.configurator.single_service_redirect_location = 'https://example.com/saml/sso'
|
|
29
|
+
expect(subject.fresh).to match('<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://example.com/saml/sso"/>')
|
|
30
|
+
expect(subject.fresh).to match('<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://example.com/saml/sso"/>')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'will not includes empty sso element' do
|
|
34
|
+
subject.configurator.single_service_post_location = ''
|
|
35
|
+
subject.configurator.single_service_redirect_location = nil
|
|
36
|
+
expect(subject.fresh).not_to match('<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"')
|
|
37
|
+
expect(subject.fresh).not_to match('<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"')
|
|
17
38
|
end
|
|
18
39
|
|
|
19
40
|
context "technical contact" do
|
|
@@ -32,31 +53,23 @@ module SamlIdp
|
|
|
32
53
|
subject.configurator.technical_contact.telephone = "1-800-555-5555"
|
|
33
54
|
subject.configurator.technical_contact.email_address = "acme@example.com"
|
|
34
55
|
|
|
35
|
-
subject.fresh.
|
|
36
|
-
'<ContactPerson contactType="technical"><Company>ACME Corporation</Company><GivenName>Road</GivenName><SurName>Runner</SurName><EmailAddress>mailto:acme@example.com</EmailAddress><TelephoneNumber>1-800-555-5555</TelephoneNumber></ContactPerson>'
|
|
37
|
-
)
|
|
56
|
+
expect(subject.fresh).to match('<ContactPerson contactType="technical"><Company>ACME Corporation</Company><GivenName>Road</GivenName><SurName>Runner</SurName><EmailAddress>mailto:acme@example.com</EmailAddress><TelephoneNumber>1-800-555-5555</TelephoneNumber></ContactPerson>')
|
|
38
57
|
end
|
|
39
58
|
|
|
40
59
|
it "no fields" do
|
|
41
|
-
subject.fresh.
|
|
42
|
-
'<ContactPerson contactType="technical"></ContactPerson>'
|
|
43
|
-
)
|
|
60
|
+
expect(subject.fresh).to match('<ContactPerson contactType="technical"></ContactPerson>')
|
|
44
61
|
end
|
|
45
62
|
|
|
46
63
|
it "just email" do
|
|
47
64
|
subject.configurator.technical_contact.email_address = "acme@example.com"
|
|
48
|
-
subject.fresh.
|
|
49
|
-
'<ContactPerson contactType="technical"><EmailAddress>mailto:acme@example.com</EmailAddress></ContactPerson>'
|
|
50
|
-
)
|
|
65
|
+
expect(subject.fresh).to match('<ContactPerson contactType="technical"><EmailAddress>mailto:acme@example.com</EmailAddress></ContactPerson>')
|
|
51
66
|
end
|
|
52
67
|
|
|
53
68
|
end
|
|
54
69
|
|
|
55
70
|
it "includes logout element as HTTP Redirect" do
|
|
56
71
|
subject.configurator.single_logout_service_redirect_location = 'https://example.com/saml/logout'
|
|
57
|
-
subject.fresh.
|
|
58
|
-
'<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://example.com/saml/logout"/>'
|
|
59
|
-
)
|
|
72
|
+
expect(subject.fresh).to match('<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://example.com/saml/logout"/>')
|
|
60
73
|
end
|
|
61
74
|
end
|
|
62
75
|
end
|
|
@@ -7,7 +7,7 @@ module SamlIdp
|
|
|
7
7
|
let(:list) { { email_address: ->() { "foo@example.com" } } }
|
|
8
8
|
|
|
9
9
|
it "has a valid all" do
|
|
10
|
-
subject.all.
|
|
10
|
+
expect(subject.all).to eq ["urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress"]
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
end
|
|
@@ -21,7 +21,7 @@ module SamlIdp
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
it "has a valid all" do
|
|
24
|
-
subject.all.
|
|
24
|
+
expect(subject.all).to eq [
|
|
25
25
|
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
|
26
26
|
"urn:oasis:names:tc:SAML:2.0:nameid-format:undefined",
|
|
27
27
|
]
|
|
@@ -32,7 +32,7 @@ module SamlIdp
|
|
|
32
32
|
let(:list) { [:email_address, :undefined] }
|
|
33
33
|
|
|
34
34
|
it "has a valid all" do
|
|
35
|
-
subject.all.
|
|
35
|
+
expect(subject.all).to eq [
|
|
36
36
|
"urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress",
|
|
37
37
|
"urn:oasis:names:tc:SAML:2.0:nameid-format:undefined",
|
|
38
38
|
]
|
|
@@ -1,106 +1,195 @@
|
|
|
1
1
|
require 'spec_helper'
|
|
2
|
-
module SamlIdp
|
|
3
|
-
describe Request do
|
|
4
|
-
let(:raw_authn_request) { "<samlp:AuthnRequest AssertionConsumerServiceURL='http://localhost:3000/saml/consume' Destination='http://localhost:1337/saml/auth' ID='_af43d1a0-e111-0130-661a-3c0754403fdb' IssueInstant='2013-08-06T22:01:35Z' Version='2.0' xmlns:samlp='urn:oasis:names:tc:SAML:2.0:protocol'><saml:Issuer xmlns:saml='urn:oasis:names:tc:SAML:2.0:assertion'>localhost:3000</saml:Issuer><samlp:NameIDPolicy AllowCreate='true' Format='urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' xmlns:samlp='urn:oasis:names:tc:SAML:2.0:protocol'/><samlp:RequestedAuthnContext Comparison='exact'><saml:AuthnContextClassRef xmlns:saml='urn:oasis:names:tc:SAML:2.0:assertion'>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef></samlp:RequestedAuthnContext></samlp:AuthnRequest>" }
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
RSpec.describe SamlIdp::Request, type: :model do
|
|
4
|
+
let(:valid_saml_request) { make_saml_request("https://foo.example.com/saml/consume", true) }
|
|
5
|
+
let(:valid_logout_request) { make_saml_sp_slo_request(security_options: { embed_sign: true })['SAMLRequest'] }
|
|
6
|
+
let(:invalid_saml_request) { "invalid_saml_request" }
|
|
7
|
+
let(:external_attributes) { { saml_request: valid_saml_request, relay_state: "state" } }
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
describe ".from_deflated_request" do
|
|
10
|
+
context "when request is valid and deflated" do
|
|
11
|
+
it "inflates and decodes the request" do
|
|
12
|
+
request = SamlIdp::Request.from_deflated_request(valid_saml_request)
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
subject.request_id.should == "_af43d1a0-e111-0130-661a-3c0754403fdb"
|
|
14
|
+
expect { Saml::XML::Document.parse(request.raw_xml) }.not_to raise_error
|
|
13
15
|
end
|
|
16
|
+
end
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
context "when request is invalid" do
|
|
19
|
+
it "returns an empty inflated string" do
|
|
20
|
+
request = SamlIdp::Request.from_deflated_request(nil)
|
|
21
|
+
expect(request.raw_xml).to eq("")
|
|
18
22
|
end
|
|
19
23
|
end
|
|
24
|
+
end
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
end
|
|
26
|
+
describe "#logout_request?" do
|
|
27
|
+
it "returns true for a valid logout request" do
|
|
28
|
+
request = SamlIdp::Request.from_deflated_request(valid_logout_request)
|
|
29
|
+
expect(request.logout_request?).to be true
|
|
30
|
+
end
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
it "returns false for a non-logout request" do
|
|
33
|
+
request = SamlIdp::Request.from_deflated_request(valid_saml_request)
|
|
34
|
+
expect(request.logout_request?).to be false
|
|
35
|
+
end
|
|
36
|
+
end
|
|
31
37
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
describe "#authn_request?" do
|
|
39
|
+
it "returns true for a valid authn request" do
|
|
40
|
+
request = SamlIdp::Request.from_deflated_request(valid_saml_request)
|
|
41
|
+
expect(request.authn_request?).to be true
|
|
42
|
+
end
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
it "returns false for a non-authn request" do
|
|
45
|
+
request = SamlIdp::Request.from_deflated_request(valid_logout_request)
|
|
46
|
+
expect(request.authn_request?).to be false
|
|
47
|
+
end
|
|
48
|
+
end
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
describe "#valid?" do
|
|
51
|
+
let(:sp_issuer) { "test_issuer" }
|
|
52
|
+
let(:valid_service_provider) do
|
|
53
|
+
instance_double(
|
|
54
|
+
"SamlIdp::ServiceProvider",
|
|
55
|
+
valid?: true,
|
|
56
|
+
acs_url: 'https://foo.example.com/saml/consume',
|
|
57
|
+
current_metadata: instance_double("Metadata", sign_authn_request?: true),
|
|
58
|
+
assertion_consumer_logout_service_url: 'https://foo.example.com/saml/logout',
|
|
59
|
+
sign_authn_request: true,
|
|
60
|
+
acceptable_response_hosts: ["foo.example.com"],
|
|
61
|
+
cert: sp_x509_cert,
|
|
62
|
+
fingerprint: SamlIdp::Fingerprint.certificate_digest(sp_x509_cert, :sha256),
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
before do
|
|
67
|
+
allow_any_instance_of(SamlIdp::Request).to receive(:service_provider).and_return(valid_service_provider)
|
|
68
|
+
allow_any_instance_of(SamlIdp::Request).to receive(:issuer).and_return(sp_issuer)
|
|
69
|
+
end
|
|
43
70
|
|
|
44
|
-
|
|
45
|
-
|
|
71
|
+
context "when the request is valid" do
|
|
72
|
+
it "returns true for a valid authn request" do
|
|
73
|
+
request = SamlIdp::Request.from_deflated_request(valid_saml_request)
|
|
74
|
+
expect(request.errors).to be_empty
|
|
75
|
+
expect(request.valid?).to be true
|
|
46
76
|
end
|
|
47
77
|
|
|
48
|
-
it "
|
|
49
|
-
|
|
78
|
+
it "returns true for a valid logout request" do
|
|
79
|
+
request = SamlIdp::Request.from_deflated_request(valid_logout_request)
|
|
80
|
+
expect(request.errors).to be_empty
|
|
81
|
+
expect(request.valid?).to be true
|
|
50
82
|
end
|
|
83
|
+
end
|
|
51
84
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
85
|
+
context 'when signature provided as external param' do
|
|
86
|
+
let!(:uri_query) { make_saml_sp_slo_request(security_options: { embed_sign: false }) }
|
|
87
|
+
let(:raw_saml_request) { uri_query['SAMLRequest'] }
|
|
88
|
+
let(:relay_state) { uri_query['RelayState'] }
|
|
89
|
+
let(:siging_algorithm) { uri_query['SigAlg'] }
|
|
90
|
+
let(:signature) { uri_query['Signature'] }
|
|
55
91
|
|
|
56
|
-
|
|
57
|
-
|
|
92
|
+
subject do
|
|
93
|
+
described_class.from_deflated_request(
|
|
94
|
+
raw_saml_request,
|
|
95
|
+
saml_request: raw_saml_request,
|
|
96
|
+
relay_state: relay_state,
|
|
97
|
+
sig_algorithm: siging_algorithm,
|
|
98
|
+
signature: signature
|
|
99
|
+
)
|
|
58
100
|
end
|
|
59
101
|
|
|
60
|
-
it "
|
|
61
|
-
subject.
|
|
102
|
+
it "should validate the request" do
|
|
103
|
+
expect(subject.valid_external_signature?).to be true
|
|
104
|
+
expect(subject.errors).to be_empty
|
|
62
105
|
end
|
|
63
106
|
|
|
64
|
-
it "
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
authn_request.issuer.should == nil
|
|
69
|
-
authn_request.valid?.should == false
|
|
107
|
+
it "should collect errors when the signature is invalid" do
|
|
108
|
+
allow(subject).to receive(:valid_external_signature?).and_return(false)
|
|
109
|
+
expect(subject.valid?).to eq(false)
|
|
110
|
+
expect(subject.errors).to include(:invalid_external_signature)
|
|
70
111
|
end
|
|
71
112
|
end
|
|
72
113
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
114
|
+
context "when the service provider is invalid" do
|
|
115
|
+
it "returns false and logs an error" do
|
|
116
|
+
allow_any_instance_of(SamlIdp::Request).to receive(:service_provider?).and_return(false)
|
|
117
|
+
request = SamlIdp::Request.from_deflated_request(valid_saml_request)
|
|
77
118
|
|
|
78
|
-
|
|
79
|
-
|
|
119
|
+
expect(request.valid?).to be false
|
|
120
|
+
expect(request.errors).to include(:sp_not_found)
|
|
80
121
|
end
|
|
122
|
+
end
|
|
81
123
|
|
|
82
|
-
|
|
83
|
-
|
|
124
|
+
context "when empty certificate for authn request validation" do
|
|
125
|
+
let(:valid_service_provider) do
|
|
126
|
+
instance_double(
|
|
127
|
+
"SamlIdp::ServiceProvider",
|
|
128
|
+
valid?: true,
|
|
129
|
+
acs_url: 'https://foo.example.com/saml/consume',
|
|
130
|
+
current_metadata: instance_double("Metadata", sign_authn_request?: true),
|
|
131
|
+
assertion_consumer_logout_service_url: 'https://foo.example.com/saml/logout',
|
|
132
|
+
sign_authn_request: true,
|
|
133
|
+
acceptable_response_hosts: ["foo.example.com"],
|
|
134
|
+
cert: nil,
|
|
135
|
+
fingerprint: nil,
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
it "returns false and logs an error" do
|
|
139
|
+
request = SamlIdp::Request.from_deflated_request(valid_saml_request)
|
|
140
|
+
|
|
141
|
+
expect(request.valid?).to be false
|
|
142
|
+
expect(request.errors).to include(:empty_certificate)
|
|
84
143
|
end
|
|
144
|
+
end
|
|
85
145
|
|
|
86
|
-
|
|
87
|
-
|
|
146
|
+
context "when empty certificate for logout validation" do
|
|
147
|
+
let(:valid_service_provider) do
|
|
148
|
+
instance_double(
|
|
149
|
+
"SamlIdp::ServiceProvider",
|
|
150
|
+
valid?: true,
|
|
151
|
+
acs_url: 'https://foo.example.com/saml/consume',
|
|
152
|
+
current_metadata: instance_double("Metadata", sign_authn_request?: true),
|
|
153
|
+
assertion_consumer_logout_service_url: 'https://foo.example.com/saml/logout',
|
|
154
|
+
sign_authn_request: true,
|
|
155
|
+
acceptable_response_hosts: ["foo.example.com"],
|
|
156
|
+
cert: nil,
|
|
157
|
+
fingerprint: nil,
|
|
158
|
+
)
|
|
88
159
|
end
|
|
89
160
|
|
|
90
|
-
|
|
91
|
-
|
|
161
|
+
before do
|
|
162
|
+
allow_any_instance_of(SamlIdp::Request).to receive(:authn_request?).and_return(false)
|
|
163
|
+
allow_any_instance_of(SamlIdp::Request).to receive(:logout_request?).and_return(true)
|
|
92
164
|
end
|
|
93
165
|
|
|
94
|
-
it "
|
|
95
|
-
|
|
166
|
+
it "returns false and logs an error" do
|
|
167
|
+
request = SamlIdp::Request.from_deflated_request(valid_saml_request)
|
|
168
|
+
|
|
169
|
+
expect(request.valid?).to be false
|
|
170
|
+
expect(request.errors).to include(:empty_certificate)
|
|
96
171
|
end
|
|
172
|
+
end
|
|
97
173
|
|
|
98
|
-
|
|
99
|
-
|
|
174
|
+
context "when both authn and logout requests are present" do
|
|
175
|
+
it "returns false and logs an error" do
|
|
176
|
+
allow_any_instance_of(SamlIdp::Request).to receive(:authn_request?).and_return(true)
|
|
177
|
+
allow_any_instance_of(SamlIdp::Request).to receive(:logout_request?).and_return(true)
|
|
178
|
+
request = SamlIdp::Request.from_deflated_request(valid_saml_request)
|
|
179
|
+
|
|
180
|
+
expect(request.valid?).to be false
|
|
181
|
+
expect(request.errors).to include(:unaccepted_request)
|
|
100
182
|
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
context "when the signature is invalid" do
|
|
186
|
+
it "returns false and logs an error" do
|
|
187
|
+
allow_any_instance_of(SamlIdp::Request).to receive(:valid_signature?).and_return(false)
|
|
188
|
+
allow_any_instance_of(SamlIdp::Request).to receive(:log)
|
|
189
|
+
request = SamlIdp::Request.from_deflated_request(valid_saml_request)
|
|
101
190
|
|
|
102
|
-
|
|
103
|
-
|
|
191
|
+
expect(request.valid?).to be false
|
|
192
|
+
expect(request.errors).to include(:invalid_embedded_signature)
|
|
104
193
|
end
|
|
105
194
|
end
|
|
106
195
|
end
|
|
@@ -6,12 +6,14 @@ module SamlIdp
|
|
|
6
6
|
let(:saml_acs_url) { "http://sportngin.com" }
|
|
7
7
|
let(:saml_request_id) { "134" }
|
|
8
8
|
let(:assertion_and_signature) { "<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"_abc\" IssueInstant=\"2013-07-31T05:00:00Z\" Version=\"2.0\"><Issuer>http://sportngin.com</Issuer><signature>stuff</signature><Subject><NameID Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\">jon.phenow@sportngin.com</NameID><SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><SubjectConfirmationData InResponseTo=\"123\" NotOnOrAfter=\"2013-07-31T05:03:00Z\" Recipient=\"http://saml.acs.url\"/></SubjectConfirmation></Subject><Conditions NotBefore=\"2013-07-31T04:59:55Z\" NotOnOrAfter=\"2013-07-31T06:00:00Z\"><AudienceRestriction><Audience>http://example.com</Audience></AudienceRestriction></Conditions><AttributeStatement><Attribute Name=\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress\"><AttributeValue>jon.phenow@sportngin.com</AttributeValue></Attribute></AttributeStatement><AuthnStatment AuthnInstant=\"2013-07-31T05:00:00Z\" SessionIndex=\"_abc\"><AuthnContext><AuthnContextClassRef>urn:federation:authentication:windows</AuthnContextClassRef></AuthnContext></AuthnStatment></Assertion>" }
|
|
9
|
+
let(:algorithm) { :sha256 }
|
|
9
10
|
subject { described_class.new(
|
|
10
11
|
response_id,
|
|
11
12
|
issuer_uri,
|
|
12
13
|
saml_acs_url,
|
|
13
14
|
saml_request_id,
|
|
14
|
-
assertion_and_signature
|
|
15
|
+
assertion_and_signature,
|
|
16
|
+
algorithm
|
|
15
17
|
) }
|
|
16
18
|
|
|
17
19
|
before do
|
|
@@ -25,7 +27,7 @@ module SamlIdp
|
|
|
25
27
|
|
|
26
28
|
it "builds a legit raw XML file" do
|
|
27
29
|
Timecop.travel(Time.zone.local(2010, 6, 1, 13, 0, 0)) do
|
|
28
|
-
subject.raw.
|
|
30
|
+
expect(subject.raw).to eq("<samlp:Response ID=\"_abc\" Version=\"2.0\" IssueInstant=\"2010-06-01T13:00:00Z\" Destination=\"http://sportngin.com\" Consent=\"urn:oasis:names:tc:SAML:2.0:consent:unspecified\" InResponseTo=\"134\" xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://example.com</Issuer><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/></samlp:Status><Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"_abc\" IssueInstant=\"2013-07-31T05:00:00Z\" Version=\"2.0\"><Issuer>http://sportngin.com</Issuer><signature>stuff</signature><Subject><NameID Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\">jon.phenow@sportngin.com</NameID><SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><SubjectConfirmationData InResponseTo=\"123\" NotOnOrAfter=\"2013-07-31T05:03:00Z\" Recipient=\"http://saml.acs.url\"/></SubjectConfirmation></Subject><Conditions NotBefore=\"2013-07-31T04:59:55Z\" NotOnOrAfter=\"2013-07-31T06:00:00Z\"><AudienceRestriction><Audience>http://example.com</Audience></AudienceRestriction></Conditions><AttributeStatement><Attribute Name=\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress\"><AttributeValue>jon.phenow@sportngin.com</AttributeValue></Attribute></AttributeStatement><AuthnStatment AuthnInstant=\"2013-07-31T05:00:00Z\" SessionIndex=\"_abc\"><AuthnContext><AuthnContextClassRef>urn:federation:authentication:windows</AuthnContextClassRef></AuthnContext></AuthnStatment></Assertion></samlp:Response>")
|
|
29
31
|
end
|
|
30
32
|
end
|
|
31
33
|
|
|
@@ -34,7 +36,7 @@ module SamlIdp
|
|
|
34
36
|
|
|
35
37
|
it "builds a legit raw XML file without a request ID" do
|
|
36
38
|
Timecop.travel(Time.zone.local(2010, 6, 1, 13, 0, 0)) do
|
|
37
|
-
subject.raw.
|
|
39
|
+
expect(subject.raw).to eq("<samlp:Response ID=\"_abc\" Version=\"2.0\" IssueInstant=\"2010-06-01T13:00:00Z\" Destination=\"http://sportngin.com\" Consent=\"urn:oasis:names:tc:SAML:2.0:consent:unspecified\" xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://example.com</Issuer><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/></samlp:Status><Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"_abc\" IssueInstant=\"2013-07-31T05:00:00Z\" Version=\"2.0\"><Issuer>http://sportngin.com</Issuer><signature>stuff</signature><Subject><NameID Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\">jon.phenow@sportngin.com</NameID><SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><SubjectConfirmationData InResponseTo=\"123\" NotOnOrAfter=\"2013-07-31T05:03:00Z\" Recipient=\"http://saml.acs.url\"/></SubjectConfirmation></Subject><Conditions NotBefore=\"2013-07-31T04:59:55Z\" NotOnOrAfter=\"2013-07-31T06:00:00Z\"><AudienceRestriction><Audience>http://example.com</Audience></AudienceRestriction></Conditions><AttributeStatement><Attribute Name=\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress\"><AttributeValue>jon.phenow@sportngin.com</AttributeValue></Attribute></AttributeStatement><AuthnStatment AuthnInstant=\"2013-07-31T05:00:00Z\" SessionIndex=\"_abc\"><AuthnContext><AuthnContextClassRef>urn:federation:authentication:windows</AuthnContextClassRef></AuthnContext></AuthnStatment></Assertion></samlp:Response>")
|
|
38
40
|
end
|
|
39
41
|
end
|
|
40
42
|
end
|