saml_idp 0.9.0 → 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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +54 -49
  3. data/lib/saml_idp/assertion_builder.rb +28 -3
  4. data/lib/saml_idp/configurator.rb +8 -3
  5. data/lib/saml_idp/controller.rb +27 -18
  6. data/lib/saml_idp/encryptor.rb +0 -1
  7. data/lib/saml_idp/fingerprint.rb +19 -0
  8. data/lib/saml_idp/incoming_metadata.rb +22 -0
  9. data/lib/saml_idp/metadata_builder.rb +25 -9
  10. data/lib/saml_idp/persisted_metadata.rb +4 -0
  11. data/lib/saml_idp/request.rb +90 -13
  12. data/lib/saml_idp/response_builder.rb +26 -6
  13. data/lib/saml_idp/saml_response.rb +62 -28
  14. data/lib/saml_idp/service_provider.rb +2 -6
  15. data/lib/saml_idp/signable.rb +1 -2
  16. data/lib/saml_idp/signature_builder.rb +2 -1
  17. data/lib/saml_idp/signed_info_builder.rb +2 -2
  18. data/lib/saml_idp/version.rb +1 -1
  19. data/lib/saml_idp/xml_security.rb +19 -14
  20. data/lib/saml_idp.rb +4 -3
  21. data/saml_idp.gemspec +32 -31
  22. data/spec/lib/saml_idp/assertion_builder_spec.rb +143 -0
  23. data/spec/lib/saml_idp/configurator_spec.rb +40 -2
  24. data/spec/lib/saml_idp/controller_spec.rb +66 -8
  25. data/spec/lib/saml_idp/fingerprint_spec.rb +14 -0
  26. data/spec/lib/saml_idp/incoming_metadata_spec.rb +89 -1
  27. data/spec/lib/saml_idp/metadata_builder_spec.rb +24 -1
  28. data/spec/lib/saml_idp/request_spec.rb +153 -64
  29. data/spec/lib/saml_idp/response_builder_spec.rb +3 -1
  30. data/spec/lib/saml_idp/saml_response_spec.rb +141 -7
  31. data/spec/rails_app/app/controllers/saml_controller.rb +1 -5
  32. data/spec/rails_app/app/controllers/saml_idp_controller.rb +55 -3
  33. data/{app → spec/rails_app/app}/views/saml_idp/idp/new.html.erb +3 -4
  34. data/{app → spec/rails_app/app}/views/saml_idp/idp/saml_post.html.erb +1 -1
  35. data/spec/rails_app/config/application.rb +1 -0
  36. data/spec/rails_app/config/boot.rb +1 -1
  37. data/spec/rails_app/config/environments/development.rb +2 -0
  38. data/spec/spec_helper.rb +20 -1
  39. data/spec/support/certificates/sp_cert_req.csr +12 -0
  40. data/spec/support/certificates/sp_private_key.pem +16 -0
  41. data/spec/support/certificates/sp_x509_cert.crt +18 -0
  42. data/spec/support/saml_request_macros.rb +105 -4
  43. data/spec/support/security_helpers.rb +12 -2
  44. data/spec/xml_security_spec.rb +11 -7
  45. metadata +96 -62
  46. data/app/controllers/saml_idp/idp_controller.rb +0 -59
@@ -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
- expect(subject.request_id).to eq("_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
- expect(req.valid?).to eq(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
- expect(subject.request_id).to eq("_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
- expect(subject.acs_url).to eq("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
- expect(subject.service_provider).to 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
- expect(subject.service_provider).to 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
- expect(subject.issuer).to eq("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
- expect(subject.valid_signature?).to 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
- expect(subject.response_url).to eq(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
- expect(subject.authn_request?).to eq(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
- expect(subject.request['ID']).to eq(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
- expect(subject.requested_authn_context).to eq("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
- expect(authn_request.issuer).to_not eq('')
68
- expect(authn_request.issuer).to be_nil
69
- expect(authn_request.valid?).to eq(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
- expect(subject.request_id).to eq('_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
- expect(subject.logout_request?).to eq(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
- expect(subject.name_id).to eq('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
- expect(subject.session_index).to eq('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
- expect(subject.issuer).to eq('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
- expect(subject.request['ID']).to eq(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
- expect(subject.response_url).to eq(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
@@ -24,6 +24,10 @@ module SamlIdp
24
24
  key_transport: 'rsa-oaep-mgf1p',
25
25
  }
26
26
  end
27
+ let(:signed_response_opts) { true }
28
+ let(:unsigned_response_opts) { false }
29
+ let(:signed_assertion_opts) { true }
30
+ let(:compress_opts) { false }
27
31
  let(:subject_encrypted) { described_class.new(reference_id,
28
32
  response_id,
29
33
  issuer_uri,
@@ -35,7 +39,12 @@ module SamlIdp
35
39
  authn_context_classref,
36
40
  expiry,
37
41
  encryption_opts,
38
- session_expiry
42
+ session_expiry,
43
+ nil,
44
+ nil,
45
+ unsigned_response_opts,
46
+ signed_assertion_opts,
47
+ compress_opts
39
48
  )
40
49
  }
41
50
 
@@ -50,7 +59,12 @@ module SamlIdp
50
59
  authn_context_classref,
51
60
  expiry,
52
61
  nil,
53
- session_expiry
62
+ session_expiry,
63
+ nil,
64
+ nil,
65
+ signed_response_opts,
66
+ signed_assertion_opts,
67
+ compress_opts
54
68
  )
55
69
  }
56
70
 
@@ -66,17 +80,137 @@ module SamlIdp
66
80
  expect(subject.build).to be_present
67
81
  end
68
82
 
69
- it "builds encrypted" do
70
- expect(subject_encrypted.build).to_not match(audience_uri)
71
- encoded_xml = subject_encrypted.build
83
+ context "encrypted" do
84
+ it "builds encrypted" do
85
+ expect(subject_encrypted.build).to_not match(audience_uri)
86
+ encoded_xml = subject_encrypted.build
87
+ resp_settings = saml_settings(saml_acs_url)
88
+ resp_settings.private_key = Default::SECRET_KEY
89
+ resp_settings.issuer = audience_uri
90
+ saml_resp = OneLogin::RubySaml::Response.new(encoded_xml, settings: resp_settings)
91
+ saml_resp.soft = false
92
+ expect(saml_resp.is_valid?).to eq(true)
93
+ end
94
+ end
95
+
96
+ context "signed response" do
97
+ let(:resp_settings) do
98
+ resp_settings = saml_settings(saml_acs_url)
99
+ resp_settings.private_key = Default::SECRET_KEY
100
+ resp_settings.issuer = audience_uri
101
+ resp_settings
102
+ end
103
+
104
+ it "will build signed valid response" do
105
+ expect { subject.build }.not_to raise_error
106
+ signed_encoded_xml = subject.build
107
+ saml_resp = OneLogin::RubySaml::Response.new(signed_encoded_xml, settings: resp_settings)
108
+ expect(
109
+ Nokogiri::XML(saml_resp.response).at_xpath(
110
+ "//p:Response//ds:Signature",
111
+ {
112
+ "p" => "urn:oasis:names:tc:SAML:2.0:protocol",
113
+ "ds" => "http://www.w3.org/2000/09/xmldsig#"
114
+ }
115
+ )).to be_present
116
+ expect(saml_resp.send(:validate_signature)).to eq(true)
117
+ expect(saml_resp.is_valid?).to eq(true)
118
+ end
119
+
120
+ context "when signed_assertion_opts is true" do
121
+ it "builds a signed assertion" do
122
+ expect { subject.build }.not_to raise_error
123
+ signed_encoded_xml = subject.build
124
+ saml_resp = OneLogin::RubySaml::Response.new(signed_encoded_xml, settings: resp_settings)
125
+ expect(
126
+ Nokogiri::XML(saml_resp.response).at_xpath(
127
+ "//p:Response//a:Assertion//ds:Signature",
128
+ {
129
+ "p" => "urn:oasis:names:tc:SAML:2.0:protocol",
130
+ "a" => "urn:oasis:names:tc:SAML:2.0:assertion",
131
+ "ds" => "http://www.w3.org/2000/09/xmldsig#"
132
+ }
133
+ )).to be_present
134
+ end
135
+ end
136
+
137
+ context "when signed_assertion_opts is false" do
138
+ let(:signed_assertion_opts) { false }
139
+
140
+ it "builds a raw assertion" do
141
+ expect { subject.build }.not_to raise_error
142
+ signed_encoded_xml = subject.build
143
+ saml_resp = OneLogin::RubySaml::Response.new(signed_encoded_xml, settings: resp_settings)
144
+ expect(
145
+ Nokogiri::XML(saml_resp.response).at_xpath(
146
+ "//p:Response//a:Assertion",
147
+ {
148
+ "p" => "urn:oasis:names:tc:SAML:2.0:protocol",
149
+ "a" => "urn:oasis:names:tc:SAML:2.0:assertion"
150
+ }
151
+ )).to be_present
152
+
153
+ expect(
154
+ Nokogiri::XML(saml_resp.response).at_xpath(
155
+ "//p:Response//Assertion//ds:Signature",
156
+ {
157
+ "p" => "urn:oasis:names:tc:SAML:2.0:protocol",
158
+ "ds" => "http://www.w3.org/2000/09/xmldsig#"
159
+ }
160
+ )).to_not be_present
161
+ end
162
+ end
163
+
164
+ context "when compress opts is true" do
165
+ let(:compress_opts) { true }
166
+ it "will build a compressed valid response" do
167
+ expect { subject.build }.not_to raise_error
168
+ compressed_signed_encoded_xml = subject.build
169
+ saml_resp = OneLogin::RubySaml::Response.new(compressed_signed_encoded_xml, settings: resp_settings)
170
+ expect(saml_resp.send(:validate_signature)).to eq(true)
171
+ expect(saml_resp.is_valid?).to eq(true)
172
+ end
173
+ end
174
+ end
175
+
176
+ it "will build signed valid response" do
177
+ expect { subject.build }.not_to raise_error
178
+ signed_encoded_xml = subject.build
72
179
  resp_settings = saml_settings(saml_acs_url)
73
180
  resp_settings.private_key = Default::SECRET_KEY
74
181
  resp_settings.issuer = audience_uri
75
- saml_resp = OneLogin::RubySaml::Response.new(encoded_xml, settings: resp_settings)
76
- saml_resp.soft = false
182
+ saml_resp = OneLogin::RubySaml::Response.new(signed_encoded_xml, settings: resp_settings)
183
+ expect(
184
+ Nokogiri::XML(saml_resp.response).at_xpath(
185
+ "//p:Response//ds:Signature",
186
+ {
187
+ "p" => "urn:oasis:names:tc:SAML:2.0:protocol",
188
+ "ds" => "http://www.w3.org/2000/09/xmldsig#"
189
+ }
190
+ )).to be_present
191
+ expect(saml_resp.send(:validate_signature)).to eq(true)
77
192
  expect(saml_resp.is_valid?).to eq(true)
78
193
  end
79
194
 
195
+ it "will pass reference_id as SessionIndex" do
196
+ expect { subject.build }.not_to raise_error
197
+ signed_encoded_xml = subject.build
198
+ resp_settings = saml_settings(saml_acs_url)
199
+ resp_settings.private_key = Default::SECRET_KEY
200
+ resp_settings.issuer = audience_uri
201
+ saml_resp = OneLogin::RubySaml::Response.new(signed_encoded_xml, settings: resp_settings)
202
+
203
+ expect(
204
+ Nokogiri::XML(saml_resp.response).at_xpath(
205
+ "//saml:AuthnStatement/@SessionIndex",
206
+ {
207
+ "samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
208
+ "saml" => "urn:oasis:names:tc:SAML:2.0:assertion"
209
+ }
210
+ ).value
211
+ ).to eq("_#{reference_id}")
212
+ end
213
+
80
214
  it "sets session expiration" do
81
215
  saml_resp = OneLogin::RubySaml::Response.new(subject.build)
82
216
  expect(saml_resp.session_expires_at).to eq Time.local(1990, "jan", 2).iso8601
@@ -2,11 +2,7 @@ class SamlController < ApplicationController
2
2
 
3
3
  def consume
4
4
  response = OneLogin::RubySaml::Response.new(params[:SAMLResponse])
5
- if Gem::Requirement.new('< 4.1') =~ Gem::Version.new(Rails.version)
6
- render :text => response.name_id
7
- else
8
- render :plain => response.name_id
9
- end
5
+ render :plain => response.name_id
10
6
  end
11
7
 
12
8
  end
@@ -1,9 +1,61 @@
1
- class SamlIdpController < SamlIdp::IdpController
1
+ class SamlIdpController < ApplicationController
2
+ include SamlIdp::Controller
3
+
4
+ if Rails::VERSION::MAJOR >= 4
5
+ before_action :add_view_path, only: [:new, :create, :logout]
6
+ before_action :validate_saml_request, only: [:new, :create, :logout]
7
+ else
8
+ before_filter :add_view_path, only: [:new, :create, :logout]
9
+ before_filter :validate_saml_request, only: [:new, :create, :logout]
10
+ end
11
+
12
+ def new
13
+ render template: "saml_idp/idp/new"
14
+ end
15
+
16
+ def show
17
+ render xml: SamlIdp.metadata.signed
18
+ end
19
+
20
+ def create
21
+ unless params[:email].blank? && params[:password].blank?
22
+ person = idp_authenticate(params[:email], params[:password])
23
+ if person.nil?
24
+ @saml_idp_fail_msg = "Incorrect email or password."
25
+ else
26
+ @saml_response = idp_make_saml_response(person)
27
+ render :template => "saml_idp/idp/saml_post", :layout => false
28
+ return
29
+ end
30
+ end
31
+ render :template => "saml_idp/idp/new"
32
+ end
33
+
34
+ def logout
35
+ idp_logout
36
+ @saml_response = idp_make_saml_response(nil)
37
+ render :template => "saml_idp/idp/saml_post", :layout => false
38
+ end
39
+
40
+ def idp_logout
41
+ raise NotImplementedError
42
+ end
43
+ private :idp_logout
44
+
2
45
  def idp_authenticate(email, password)
3
46
  { :email => email }
4
47
  end
48
+ protected :idp_authenticate
5
49
 
6
- def idp_make_saml_response(user)
7
- encode_response(user[:email])
50
+ def idp_make_saml_response(person)
51
+ encode_response(person[:email])
8
52
  end
53
+ protected :idp_make_saml_response
54
+
55
+ private
56
+
57
+ def add_view_path
58
+ prepend_view_path("app/views")
59
+ end
60
+
9
61
  end
@@ -1,22 +1,21 @@
1
1
  <% if @saml_idp_fail_msg %>
2
2
  <div id="saml_idp_fail_msg" class="flash error"><%= @saml_idp_fail_msg %></div>
3
3
  <% end %>
4
-
5
4
  <%= form_tag do %>
6
5
  <%= hidden_field_tag("SAMLRequest", params[:SAMLRequest]) %>
7
6
  <%= hidden_field_tag("RelayState", params[:RelayState]) %>
7
+ <%= hidden_field_tag("SigAlg", params[:SigAlg]) %>
8
+ <%= hidden_field_tag("Signature", params[:Signature]) %>
8
9
 
9
10
  <p>
10
11
  <%= label_tag :email %>
11
12
  <%= email_field_tag :email, params[:email], :autocapitalize => "off", :autocorrect => "off", :autofocus => "autofocus", :spellcheck => "false", :size => 30, :class => "email_pwd txt" %>
12
13
  </p>
13
-
14
14
  <p>
15
15
  <%= label_tag :password %>
16
16
  <%= password_field_tag :password, params[:password], :autocapitalize => "off", :autocorrect => "off", :spellcheck => "false", :size => 30, :class => "email_pwd txt" %>
17
17
  </p>
18
-
19
18
  <p>
20
19
  <%= submit_tag "Sign in", :class => "button big blueish" %>
21
20
  </p>
22
- <% end %>
21
+ <% end %>
@@ -11,4 +11,4 @@
11
11
  <%= submit_tag "Submit" %>
12
12
  <% end %>
13
13
  </body>
14
- </html>
14
+ </html>
@@ -18,6 +18,7 @@ module RailsApp
18
18
 
19
19
  # Custom directories with classes and modules you want to be autoloadable.
20
20
  # config.autoload_paths += %W(#{config.root}/extras)
21
+ config.autoload_paths += %w[app/controllers]
21
22
 
22
23
  # Only load the plugins named here, in the order given (default is alphabetical).
23
24
  # :all can be used as a placeholder for all plugins not explicitly named.
@@ -3,4 +3,4 @@ require 'rubygems'
3
3
  # Set up gems listed in the Gemfile.
4
4
  ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
5
5
 
6
- require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
6
+ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
@@ -29,4 +29,6 @@ RailsApp::Application.configure do
29
29
  # Log the query plan for queries taking more than this (works
30
30
  # with SQLite, MySQL, and PostgreSQL)
31
31
  #config.active_record.auto_explain_threshold_in_seconds = 0.5
32
+
33
+ config.hosts << "foo.example.com" if config.respond_to?(:hosts)
32
34
  end
data/spec/spec_helper.rb CHANGED
@@ -43,9 +43,28 @@ RSpec.configure do |config|
43
43
  }
44
44
  end
45
45
  end
46
+
47
+ # To reset to default config
48
+ config.after do
49
+ SamlIdp.instance_variable_set(:@config, nil)
50
+ SamlIdp.configure do |c|
51
+ c.attributes = {
52
+ emailAddress: {
53
+ name: "email-address",
54
+ getter: ->(p) { "foo@example.com" }
55
+ }
56
+ }
57
+
58
+ c.name_id.formats = {
59
+ "1.1" => {
60
+ email_address: ->(p) { "foo@example.com" }
61
+ }
62
+ }
63
+ end
64
+ end
46
65
  end
47
66
 
48
67
  SamlIdp::Default::SERVICE_PROVIDER[:metadata_url] = 'https://example.com/meta'
49
68
  SamlIdp::Default::SERVICE_PROVIDER[:response_hosts] = ['foo.example.com']
50
69
  SamlIdp::Default::SERVICE_PROVIDER[:assertion_consumer_logout_service_url] = 'https://foo.example.com/saml/logout'
51
- Capybara.default_host = "https://app.example.com"
70
+ Capybara.default_host = "https://foo.example.com"
@@ -0,0 +1,12 @@
1
+ -----BEGIN CERTIFICATE REQUEST-----
2
+ MIIByTCCATICAQAwgYgxCzAJBgNVBAYTAmpwMQ4wDAYDVQQIDAVUb2t5bzELMAkG
3
+ A1UECgwCR1MxIDAeBgNVBAMMF2h0dHBzOi8vZm9vLmV4YW1wbGUuY29tMQwwCgYD
4
+ VQQHDANGb28xDDAKBgNVBAsMA0JvbzEeMBwGCSqGSIb3DQEJARYPZm9vQGV4YW1w
5
+ bGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8DVj2mVLQV7AjT+cn
6
+ Lv3kDnQFvAo3RdUeGGhplsYFacYByzNRD/jeguu1ahrvznDyZN8p3yB7OPbmt0r0
7
+ aGr+yYzPh6brgkf5u6FMtWTj94vLQuT/uyQGuzdBkiLb5mAWRMtm43oHXDK0v25J
8
+ tsG1PJnntkXfBDpFP1eWLO+jZwIDAQABoAAwDQYJKoZIhvcNAQENBQADgYEAd/J6
9
+ 5zjrMhgjxuaMuWCiNN7IS4F9SKy+gEmhkpNVCpChbpggruaEIoERjDP/TkZn2dgL
10
+ VUeHTZB92t+wWfQbHNvEfbzqlV3XkuHkxewCwofnIV/k+8zG1Al5ELSKHehItxig
11
+ rnTuBrFYsd2j4HEVqLzm4NyCfL+xzn/D4U2ec50=
12
+ -----END CERTIFICATE REQUEST-----
@@ -0,0 +1,16 @@
1
+ -----BEGIN PRIVATE KEY-----
2
+ MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALwNWPaZUtBXsCNP
3
+ 5ycu/eQOdAW8CjdF1R4YaGmWxgVpxgHLM1EP+N6C67VqGu/OcPJk3ynfIHs49ua3
4
+ SvRoav7JjM+HpuuCR/m7oUy1ZOP3i8tC5P+7JAa7N0GSItvmYBZEy2bjegdcMrS/
5
+ bkm2wbU8mee2Rd8EOkU/V5Ys76NnAgMBAAECgYEArwclVHCkebIECPnnxbqhKNCj
6
+ AGtifsuKbrZ9CDoDGSq31xeQLdTV6BSm2nVlmOnmilWEuG4qx0Xf2CGlrBI78kmv
7
+ vHCfFdaGnTxbmYnD0HN0u4RK2trsxWO+rEkJk14JE2eVD6ZRPrq1UOSMgGPrQSMb
8
+ SuwAHUu/j94eL8BXuhECQQD3jTlo3Y4VPWttP6XPNqKDP+jRYJs5G0Bch//S9Qy7
9
+ QzmU9/yAUk0BEOyqYcLxinjJhoq6bR2fiIibn+77z3jtAkEAwnhLwkGYOb7Nt3V6
10
+ dQLKx1BP9dnYH7qG/sCmAs7GHPv4LGluaz4zsh2pdEDF/Xar4gwTzUpxYo8FpkCH
11
+ rf4nIwJAVfWnGr/cR4nVVNFGHUcGdXbqvFHEdLb+yWK8NZ+79Qap5w2Zk2GAtb8P
12
+ vzZFQCRqPuhGIegj4jLB5PBLRwtLHQJBAJiWyWL4ExikRUhBTr/HXBL+Sm9u6i0j
13
+ L89unBQx6LNPZhB6/Z/6Y5fLvG2ycWgLGJ06usLnOYaLEHS9x3hXpp8CQQCdtQHw
14
+ xeLBPhRDpfWWbSmFr+bFxyD/4iQHTHToIs3kaecn6OJ4rczIFpGm2Bm7f4X7F3H3
15
+ DDy4jZ0R6iDqCcQD
16
+ -----END PRIVATE KEY-----