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.
Files changed (59) hide show
  1. checksums.yaml +5 -5
  2. data/Gemfile +1 -1
  3. data/README.md +71 -55
  4. data/lib/saml_idp/assertion_builder.rb +28 -3
  5. data/lib/saml_idp/configurator.rb +9 -3
  6. data/lib/saml_idp/controller.rb +27 -16
  7. data/lib/saml_idp/encryptor.rb +0 -1
  8. data/lib/saml_idp/fingerprint.rb +19 -0
  9. data/lib/saml_idp/incoming_metadata.rb +31 -1
  10. data/lib/saml_idp/metadata_builder.rb +25 -9
  11. data/lib/saml_idp/persisted_metadata.rb +4 -0
  12. data/lib/saml_idp/request.rb +103 -13
  13. data/lib/saml_idp/response_builder.rb +26 -6
  14. data/lib/saml_idp/saml_response.rb +62 -28
  15. data/lib/saml_idp/service_provider.rb +16 -6
  16. data/lib/saml_idp/signable.rb +1 -2
  17. data/lib/saml_idp/signature_builder.rb +2 -1
  18. data/lib/saml_idp/signed_info_builder.rb +2 -2
  19. data/lib/saml_idp/version.rb +1 -1
  20. data/lib/saml_idp/xml_security.rb +20 -15
  21. data/lib/saml_idp.rb +4 -3
  22. data/saml_idp.gemspec +46 -42
  23. data/spec/acceptance/idp_controller_spec.rb +5 -4
  24. data/spec/lib/saml_idp/algorithmable_spec.rb +6 -6
  25. data/spec/lib/saml_idp/assertion_builder_spec.rb +151 -8
  26. data/spec/lib/saml_idp/attribute_decorator_spec.rb +8 -8
  27. data/spec/lib/saml_idp/configurator_spec.rb +45 -7
  28. data/spec/lib/saml_idp/controller_spec.rb +86 -25
  29. data/spec/lib/saml_idp/encryptor_spec.rb +4 -4
  30. data/spec/lib/saml_idp/fingerprint_spec.rb +14 -0
  31. data/spec/lib/saml_idp/incoming_metadata_spec.rb +134 -0
  32. data/spec/lib/saml_idp/metadata_builder_spec.rb +30 -17
  33. data/spec/lib/saml_idp/name_id_formatter_spec.rb +3 -3
  34. data/spec/lib/saml_idp/request_spec.rb +153 -64
  35. data/spec/lib/saml_idp/response_builder_spec.rb +5 -3
  36. data/spec/lib/saml_idp/saml_response_spec.rb +146 -12
  37. data/spec/lib/saml_idp/service_provider_spec.rb +2 -2
  38. data/spec/lib/saml_idp/signable_spec.rb +1 -1
  39. data/spec/lib/saml_idp/signature_builder_spec.rb +2 -2
  40. data/spec/lib/saml_idp/signed_info_builder_spec.rb +3 -3
  41. data/spec/rails_app/app/controllers/saml_controller.rb +1 -1
  42. data/spec/rails_app/app/controllers/saml_idp_controller.rb +55 -3
  43. data/{app → spec/rails_app/app}/views/saml_idp/idp/new.html.erb +3 -4
  44. data/{app → spec/rails_app/app}/views/saml_idp/idp/saml_post.html.erb +1 -1
  45. data/spec/rails_app/config/application.rb +1 -6
  46. data/spec/rails_app/config/boot.rb +1 -1
  47. data/spec/rails_app/config/environments/development.rb +2 -5
  48. data/spec/rails_app/config/environments/production.rb +1 -0
  49. data/spec/rails_app/config/environments/test.rb +1 -0
  50. data/spec/spec_helper.rb +23 -1
  51. data/spec/support/certificates/sp_cert_req.csr +12 -0
  52. data/spec/support/certificates/sp_private_key.pem +16 -0
  53. data/spec/support/certificates/sp_x509_cert.crt +18 -0
  54. data/spec/support/saml_request_macros.rb +107 -5
  55. data/spec/support/security_helpers.rb +12 -2
  56. data/spec/xml_security_spec.rb +19 -15
  57. metadata +146 -80
  58. data/app/controllers/saml_idp/idp_controller.rb +0 -59
  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.should_not be_empty
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).should be_truthy
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.fresh.should match(
15
- '<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://example.com/saml/logout"/>'
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.should match(
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.should match(
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.should match(
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.should match(
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.should == ["urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress"]
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.should == [
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.should == [
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
- describe "deflated request" do
7
- let(:deflated_request) { Base64.encode64(Zlib::Deflate.deflate(raw_authn_request, 9)[2..-5]) }
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
- subject { described_class.from_deflated_request deflated_request }
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
- it "inflates" do
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
- it "handles invalid SAML" do
16
- req = described_class.from_deflated_request "bang!"
17
- req.valid?.should == false
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
- describe "authn request" do
22
- subject { described_class.new raw_authn_request }
23
-
24
- it "has a valid request_id" do
25
- subject.request_id.should == "_af43d1a0-e111-0130-661a-3c0754403fdb"
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
- it "has a valid acs_url" do
29
- subject.acs_url.should == "http://localhost:3000/saml/consume"
30
- end
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
- it "has a valid service_provider" do
33
- subject.service_provider.should be_a ServiceProvider
34
- end
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
- it "has a valid service_provider" do
37
- subject.service_provider.should be_truthy
38
- end
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
- it "has a valid issuer" do
41
- subject.issuer.should == "localhost:3000"
42
- end
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
- it "has a valid valid_signature" do
45
- subject.valid_signature?.should be_truthy
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 "should return acs_url for response_url" do
49
- subject.response_url.should == subject.acs_url
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
- it "is a authn request" do
53
- subject.authn_request?.should == true
54
- end
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
- it "fetches internal request" do
57
- subject.request['ID'].should == subject.request_id
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 "has a valid authn context" do
61
- subject.requested_authn_context.should == "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
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 "does not permit empty issuer" do
65
- raw_req = raw_authn_request.gsub('localhost:3000', '')
66
- authn_request = described_class.new raw_req
67
- authn_request.issuer.should_not == ''
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
- describe "logout request" do
74
- let(:raw_logout_request) { "<LogoutRequest ID='_some_response_id' Version='2.0' IssueInstant='2010-06-01T13:00:00Z' Destination='http://localhost:3000/saml/logout' xmlns='urn:oasis:names:tc:SAML:2.0:protocol'><Issuer xmlns='urn:oasis:names:tc:SAML:2.0:assertion'>http://example.com</Issuer><NameID xmlns='urn:oasis:names:tc:SAML:2.0:assertion' Format='urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'>some_name_id</NameID><SessionIndex>abc123index</SessionIndex></LogoutRequest>" }
75
-
76
- subject { described_class.new raw_logout_request }
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
- it "has a valid request_id" do
79
- subject.request_id.should == '_some_response_id'
119
+ expect(request.valid?).to be false
120
+ expect(request.errors).to include(:sp_not_found)
80
121
  end
122
+ end
81
123
 
82
- it "should be flagged as a logout_request" do
83
- subject.logout_request?.should == true
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
- it "should have a valid name_id" do
87
- subject.name_id.should == 'some_name_id'
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
- it "should have a session index" do
91
- subject.session_index.should == 'abc123index'
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 "should have a valid issuer" do
95
- subject.issuer.should == 'http://example.com'
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
- it "fetches internal request" do
99
- subject.request['ID'].should == subject.request_id
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
- it "should return logout_url for response_url" do
103
- subject.response_url.should == subject.logout_url
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.should == "<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>"
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.should == "<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>"
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