saml_idp 0.15.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.
- checksums.yaml +4 -4
- data/README.md +12 -4
- data/lib/saml_idp/configurator.rb +3 -3
- data/lib/saml_idp/controller.rb +12 -9
- data/lib/saml_idp/incoming_metadata.rb +9 -0
- data/lib/saml_idp/metadata_builder.rb +2 -1
- data/lib/saml_idp/request.rb +84 -14
- data/lib/saml_idp/saml_response.rb +2 -2
- data/lib/saml_idp/service_provider.rb +1 -0
- 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 +19 -14
- data/lib/saml_idp.rb +3 -3
- data/saml_idp.gemspec +4 -3
- data/spec/lib/saml_idp/configurator_spec.rb +38 -2
- data/spec/lib/saml_idp/controller_spec.rb +43 -9
- data/spec/lib/saml_idp/incoming_metadata_spec.rb +75 -1
- data/spec/lib/saml_idp/metadata_builder_spec.rb +1 -1
- data/spec/lib/saml_idp/request_spec.rb +152 -97
- data/spec/lib/saml_idp/saml_response_spec.rb +19 -0
- data/spec/rails_app/app/views/saml_idp/idp/new.html.erb +3 -0
- data/spec/support/saml_request_macros.rb +60 -18
- data/spec/support/security_helpers.rb +2 -2
- data/spec/xml_security_spec.rb +11 -7
- metadata +32 -20
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
|
2
3
|
module SamlIdp
|
3
4
|
|
4
5
|
metadata_1 = <<-eos
|
@@ -29,11 +30,52 @@ module SamlIdp
|
|
29
30
|
</md:EntityDescriptor>
|
30
31
|
eos
|
31
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
|
+
|
32
75
|
describe IncomingMetadata do
|
33
76
|
it 'should properly set sign_assertions to false' do
|
34
77
|
metadata = SamlIdp::IncomingMetadata.new(metadata_1)
|
35
78
|
expect(metadata.sign_assertions).to eq(false)
|
36
|
-
expect(metadata.sign_authn_request).to eq(false)
|
37
79
|
end
|
38
80
|
|
39
81
|
it 'should properly set entity_id as https://test-saml.com/saml' do
|
@@ -56,5 +98,37 @@ module SamlIdp
|
|
56
98
|
metadata = SamlIdp::IncomingMetadata.new(metadata_4)
|
57
99
|
expect(metadata.sign_authn_request).to eq(false)
|
58
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
|
59
133
|
end
|
60
134
|
end
|
@@ -6,7 +6,7 @@ module SamlIdp
|
|
6
6
|
end
|
7
7
|
|
8
8
|
it "signs valid xml" do
|
9
|
-
expect(Saml::XML::Document.parse(subject.signed).valid_signature?(Default::FINGERPRINT)).to 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
|
@@ -1,140 +1,195 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
-
module SamlIdp
|
3
|
-
describe Request do
|
4
|
-
let(:issuer) { 'localhost:3000' }
|
5
|
-
let(:raw_authn_request) do
|
6
|
-
"<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'>#{issuer}</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>"
|
7
|
-
end
|
8
2
|
|
9
|
-
|
10
|
-
|
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" } }
|
11
8
|
|
12
|
-
|
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)
|
13
13
|
|
14
|
-
|
15
|
-
expect(subject.request_id).to eq("_af43d1a0-e111-0130-661a-3c0754403fdb")
|
14
|
+
expect { Saml::XML::Document.parse(request.raw_xml) }.not_to raise_error
|
16
15
|
end
|
16
|
+
end
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
|
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("")
|
21
22
|
end
|
22
23
|
end
|
24
|
+
end
|
23
25
|
|
24
|
-
|
25
|
-
|
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
|
26
31
|
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
30
37
|
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
34
43
|
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
38
49
|
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
42
70
|
|
43
|
-
|
44
|
-
|
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
|
45
76
|
end
|
46
77
|
|
47
|
-
it "
|
48
|
-
|
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
|
49
82
|
end
|
83
|
+
end
|
50
84
|
|
51
|
-
|
52
|
-
|
53
|
-
|
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'] }
|
54
91
|
|
55
|
-
|
56
|
-
|
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
|
+
)
|
57
100
|
end
|
58
101
|
|
59
|
-
it "
|
60
|
-
expect(subject.
|
102
|
+
it "should validate the request" do
|
103
|
+
expect(subject.valid_external_signature?).to be true
|
104
|
+
expect(subject.errors).to be_empty
|
61
105
|
end
|
62
106
|
|
63
|
-
it
|
64
|
-
|
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)
|
65
111
|
end
|
112
|
+
end
|
66
113
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
before do
|
72
|
-
allow(SamlIdp.config).to receive(:logger).and_return(logger)
|
73
|
-
end
|
74
|
-
|
75
|
-
it 'is invalid' do
|
76
|
-
expect(subject.issuer).to_not eq('')
|
77
|
-
expect(subject.issuer).to be_nil
|
78
|
-
expect(subject.valid?).to eq(false)
|
79
|
-
end
|
80
|
-
|
81
|
-
context 'a Ruby Logger is configured' do
|
82
|
-
let(:logger) { Logger.new($stdout) }
|
83
|
-
|
84
|
-
before do
|
85
|
-
allow(logger).to receive(:info)
|
86
|
-
end
|
87
|
-
|
88
|
-
it 'logs an error message' do
|
89
|
-
expect(subject.valid?).to be false
|
90
|
-
expect(logger).to have_received(:info).with('Unable to find service provider for issuer ')
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
context 'a logger lambda is configured' do
|
95
|
-
let(:logger) { double }
|
96
|
-
|
97
|
-
before { allow(logger).to receive(:call) }
|
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)
|
98
118
|
|
99
|
-
|
100
|
-
|
101
|
-
expect(logger).to have_received(:call).with('Unable to find service provider for issuer ')
|
102
|
-
end
|
103
|
-
end
|
119
|
+
expect(request.valid?).to be false
|
120
|
+
expect(request.errors).to include(:sp_not_found)
|
104
121
|
end
|
105
122
|
end
|
106
123
|
|
107
|
-
|
108
|
-
let(:
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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)
|
114
143
|
end
|
144
|
+
end
|
115
145
|
|
116
|
-
|
117
|
-
|
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
|
+
)
|
118
159
|
end
|
119
160
|
|
120
|
-
|
121
|
-
|
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)
|
122
164
|
end
|
123
165
|
|
124
|
-
it "
|
125
|
-
|
126
|
-
end
|
166
|
+
it "returns false and logs an error" do
|
167
|
+
request = SamlIdp::Request.from_deflated_request(valid_saml_request)
|
127
168
|
|
128
|
-
|
129
|
-
expect(
|
169
|
+
expect(request.valid?).to be false
|
170
|
+
expect(request.errors).to include(:empty_certificate)
|
130
171
|
end
|
172
|
+
end
|
173
|
+
|
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)
|
131
179
|
|
132
|
-
|
133
|
-
expect(
|
180
|
+
expect(request.valid?).to be false
|
181
|
+
expect(request.errors).to include(:unaccepted_request)
|
134
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)
|
135
190
|
|
136
|
-
|
137
|
-
expect(
|
191
|
+
expect(request.valid?).to be false
|
192
|
+
expect(request.errors).to include(:invalid_embedded_signature)
|
138
193
|
end
|
139
194
|
end
|
140
195
|
end
|
@@ -192,6 +192,25 @@ module SamlIdp
|
|
192
192
|
expect(saml_resp.is_valid?).to eq(true)
|
193
193
|
end
|
194
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
|
+
|
195
214
|
it "sets session expiration" do
|
196
215
|
saml_resp = OneLogin::RubySaml::Response.new(subject.build)
|
197
216
|
expect(saml_resp.session_expires_at).to eq Time.local(1990, "jan", 2).iso8601
|
@@ -4,6 +4,9 @@
|
|
4
4
|
<%= form_tag do %>
|
5
5
|
<%= hidden_field_tag("SAMLRequest", params[:SAMLRequest]) %>
|
6
6
|
<%= hidden_field_tag("RelayState", params[:RelayState]) %>
|
7
|
+
<%= hidden_field_tag("SigAlg", params[:SigAlg]) %>
|
8
|
+
<%= hidden_field_tag("Signature", params[:Signature]) %>
|
9
|
+
|
7
10
|
<p>
|
8
11
|
<%= label_tag :email %>
|
9
12
|
<%= email_field_tag :email, params[:email], :autocapitalize => "off", :autocorrect => "off", :autofocus => "autofocus", :spellcheck => "false", :size => 30, :class => "email_pwd txt" %>
|
@@ -3,8 +3,8 @@ require 'saml_idp/logout_request_builder'
|
|
3
3
|
module SamlRequestMacros
|
4
4
|
def make_saml_request(requested_saml_acs_url = "https://foo.example.com/saml/consume", enable_secure_options = false)
|
5
5
|
auth_request = OneLogin::RubySaml::Authrequest.new
|
6
|
-
auth_url = auth_request.
|
7
|
-
|
6
|
+
auth_url = auth_request.create_params(saml_settings(requested_saml_acs_url, enable_secure_options))
|
7
|
+
auth_url['SAMLRequest']
|
8
8
|
end
|
9
9
|
|
10
10
|
def make_saml_logout_request(requested_saml_logout_url = 'https://foo.example.com/saml/logout')
|
@@ -18,39 +18,46 @@ module SamlRequestMacros
|
|
18
18
|
Base64.strict_encode64(request_builder.signed)
|
19
19
|
end
|
20
20
|
|
21
|
+
def make_saml_sp_slo_request(param_type: true, security_options: {})
|
22
|
+
logout_request = OneLogin::RubySaml::Logoutrequest.new
|
23
|
+
saml_sp_setting = saml_settings("https://foo.example.com/saml/consume", true, security_options: security_options)
|
24
|
+
if param_type
|
25
|
+
logout_request.create_params(saml_sp_setting, 'RelayState' => 'https://foo.example.com/home')
|
26
|
+
else
|
27
|
+
logout_request.create(saml_sp_setting, 'RelayState' => 'https://foo.example.com/home')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
21
31
|
def generate_sp_metadata(saml_acs_url = "https://foo.example.com/saml/consume", enable_secure_options = false)
|
22
32
|
sp_metadata = OneLogin::RubySaml::Metadata.new
|
23
33
|
sp_metadata.generate(saml_settings(saml_acs_url, enable_secure_options), true)
|
24
34
|
end
|
25
35
|
|
26
|
-
def saml_settings(saml_acs_url = "https://foo.example.com/saml/consume", enable_secure_options = false)
|
36
|
+
def saml_settings(saml_acs_url = "https://foo.example.com/saml/consume", enable_secure_options = false, security_options: {})
|
27
37
|
settings = OneLogin::RubySaml::Settings.new
|
28
38
|
settings.assertion_consumer_service_url = saml_acs_url
|
29
39
|
settings.issuer = "http://example.com/issuer"
|
30
40
|
settings.idp_sso_target_url = "http://idp.com/saml/idp"
|
41
|
+
settings.idp_slo_target_url = "http://idp.com/saml/slo"
|
31
42
|
settings.assertion_consumer_logout_service_url = 'https://foo.example.com/saml/logout'
|
32
43
|
settings.idp_cert_fingerprint = SamlIdp::Default::FINGERPRINT
|
33
44
|
settings.name_identifier_format = SamlIdp::Default::NAME_ID_FORMAT
|
34
|
-
add_securty_options(settings) if enable_secure_options
|
45
|
+
add_securty_options(settings, default_sp_security_options.merge!(security_options)) if enable_secure_options
|
35
46
|
settings
|
36
47
|
end
|
37
48
|
|
38
|
-
def add_securty_options(settings,
|
39
|
-
embed_sign: true,
|
40
|
-
logout_requests_signed: true,
|
41
|
-
logout_responses_signed: true,
|
42
|
-
digest_method: XMLSecurity::Document::SHA256,
|
43
|
-
signature_method: XMLSecurity::Document::RSA_SHA256)
|
49
|
+
def add_securty_options(settings, options = default_sp_security_options)
|
44
50
|
# Security section
|
45
51
|
settings.idp_cert = SamlIdp::Default::X509_CERTIFICATE
|
46
52
|
# Signed embedded singature
|
47
|
-
settings.security[:authn_requests_signed] = authn_requests_signed
|
48
|
-
settings.security[:embed_sign] = embed_sign
|
49
|
-
settings.security[:logout_requests_signed] = logout_requests_signed
|
50
|
-
settings.security[:logout_responses_signed] = logout_responses_signed
|
51
|
-
settings.security[:metadata_signed] = digest_method
|
52
|
-
settings.security[:digest_method] = digest_method
|
53
|
-
settings.security[:signature_method] = signature_method
|
53
|
+
settings.security[:authn_requests_signed] = options[:authn_requests_signed]
|
54
|
+
settings.security[:embed_sign] = options[:embed_sign]
|
55
|
+
settings.security[:logout_requests_signed] = options[:logout_requests_signed]
|
56
|
+
settings.security[:logout_responses_signed] = options[:logout_responses_signed]
|
57
|
+
settings.security[:metadata_signed] = options[:digest_method]
|
58
|
+
settings.security[:digest_method] = options[:digest_method]
|
59
|
+
settings.security[:signature_method] = options[:signature_method]
|
60
|
+
settings.security[:want_assertions_signed] = options[:assertions_signed]
|
54
61
|
settings.private_key = sp_pv_key
|
55
62
|
settings.certificate = sp_x509_cert
|
56
63
|
end
|
@@ -82,16 +89,51 @@ module SamlRequestMacros
|
|
82
89
|
response_hosts: [URI(saml_acs_url).host],
|
83
90
|
acs_url: saml_acs_url,
|
84
91
|
cert: sp_x509_cert,
|
85
|
-
fingerprint: SamlIdp::Fingerprint.certificate_digest(sp_x509_cert)
|
92
|
+
fingerprint: SamlIdp::Fingerprint.certificate_digest(sp_x509_cert),
|
93
|
+
assertion_consumer_logout_service_url: 'https://foo.example.com/saml/logout'
|
86
94
|
}
|
87
95
|
}
|
88
96
|
end
|
89
97
|
end
|
90
98
|
|
99
|
+
def decode_saml_request(saml_request)
|
100
|
+
decoded_request = Base64.decode64(saml_request)
|
101
|
+
begin
|
102
|
+
# Try to decompress, since SAMLRequest might be compressed
|
103
|
+
Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(decoded_request)
|
104
|
+
rescue Zlib::DataError
|
105
|
+
# If it's not compressed, just return the decoded request
|
106
|
+
decoded_request
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
91
110
|
def print_pretty_xml(xml_string)
|
92
111
|
doc = REXML::Document.new xml_string
|
93
112
|
outbuf = ""
|
94
113
|
doc.write(outbuf, 1)
|
95
114
|
puts outbuf
|
96
115
|
end
|
116
|
+
|
117
|
+
def decode_saml_request(saml_request)
|
118
|
+
decoded_request = Base64.decode64(saml_request)
|
119
|
+
begin
|
120
|
+
# Try to decompress, since SAMLRequest might be compressed
|
121
|
+
Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(decoded_request)
|
122
|
+
rescue Zlib::DataError
|
123
|
+
# If it's not compressed, just return the decoded request
|
124
|
+
decoded_request
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def default_sp_security_options
|
129
|
+
{
|
130
|
+
authn_requests_signed: true,
|
131
|
+
embed_sign: true,
|
132
|
+
logout_requests_signed: true,
|
133
|
+
logout_responses_signed: true,
|
134
|
+
digest_method: XMLSecurity::Document::SHA256,
|
135
|
+
signature_method: XMLSecurity::Document::RSA_SHA256,
|
136
|
+
assertions_signed: true
|
137
|
+
}
|
138
|
+
end
|
97
139
|
end
|
@@ -51,8 +51,8 @@ module SecurityHelpers
|
|
51
51
|
@signature_fingerprint1 ||= "C5:19:85:D9:47:F1:BE:57:08:20:25:05:08:46:EB:27:F6:CA:B7:83"
|
52
52
|
end
|
53
53
|
|
54
|
-
def
|
55
|
-
@
|
54
|
+
def certificate_1
|
55
|
+
@certificate_1 ||= File.read(File.join(File.dirname(__FILE__), 'certificates', 'certificate1'))
|
56
56
|
end
|
57
57
|
|
58
58
|
def r1_signature_2
|
data/spec/xml_security_spec.rb
CHANGED
@@ -19,7 +19,7 @@ module SamlIdp
|
|
19
19
|
end
|
20
20
|
|
21
21
|
it "it raise Fingerprint mismatch" do
|
22
|
-
expect { document.validate("no:fi:ng:er:pr:in:t", false) }.to(
|
22
|
+
expect { document.validate("", "no:fi:ng:er:pr:in:t", false) }.to(
|
23
23
|
raise_error(SamlIdp::XMLSecurity::SignedDocument::ValidationError, "Fingerprint mismatch")
|
24
24
|
)
|
25
25
|
end
|
@@ -45,10 +45,10 @@ module SamlIdp
|
|
45
45
|
response = Base64.decode64(response_document)
|
46
46
|
response.sub!(/<ds:X509Certificate>.*<\/ds:X509Certificate>/, "")
|
47
47
|
document = XMLSecurity::SignedDocument.new(response)
|
48
|
-
expect { document.validate("a fingerprint", false) }.to(
|
48
|
+
expect { document.validate("", "a fingerprint", false) }.to(
|
49
49
|
raise_error(
|
50
50
|
SamlIdp::XMLSecurity::SignedDocument::ValidationError,
|
51
|
-
"Certificate
|
51
|
+
"Certificate validation is required, but it doesn't exist."
|
52
52
|
)
|
53
53
|
)
|
54
54
|
end
|
@@ -57,22 +57,26 @@ module SamlIdp
|
|
57
57
|
describe "Algorithms" do
|
58
58
|
it "validate using SHA1" do
|
59
59
|
document = XMLSecurity::SignedDocument.new(fixture(:adfs_response_sha1, false))
|
60
|
-
|
60
|
+
base64cert = document.elements["//ds:X509Certificate"].text
|
61
|
+
expect(document.validate(base64cert, "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72")).to be_truthy
|
61
62
|
end
|
62
63
|
|
63
64
|
it "validate using SHA256" do
|
64
65
|
document = XMLSecurity::SignedDocument.new(fixture(:adfs_response_sha256, false))
|
65
|
-
|
66
|
+
base64cert = document.elements["//ds:X509Certificate"].text
|
67
|
+
expect(document.validate(base64cert, "28:74:9B:E8:1F:E8:10:9C:A8:7C:A9:C3:E3:C5:01:6C:92:1C:B4:BA")).to be_truthy
|
66
68
|
end
|
67
69
|
|
68
70
|
it "validate using SHA384" do
|
69
71
|
document = XMLSecurity::SignedDocument.new(fixture(:adfs_response_sha384, false))
|
70
|
-
|
72
|
+
base64cert = document.elements["//ds:X509Certificate"].text
|
73
|
+
expect(document.validate(base64cert, "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72")).to be_truthy
|
71
74
|
end
|
72
75
|
|
73
76
|
it "validate using SHA512" do
|
74
77
|
document = XMLSecurity::SignedDocument.new(fixture(:adfs_response_sha512, false))
|
75
|
-
|
78
|
+
base64cert = document.elements["//ds:X509Certificate"].text
|
79
|
+
expect(document.validate(base64cert, "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72")).to be_truthy
|
76
80
|
end
|
77
81
|
end
|
78
82
|
|