saml_idp 0.16.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 -3
- data/lib/saml_idp/configurator.rb +3 -3
- data/lib/saml_idp/controller.rb +11 -8
- data/lib/saml_idp/incoming_metadata.rb +9 -0
- data/lib/saml_idp/metadata_builder.rb +2 -1
- data/lib/saml_idp/request.rb +81 -11
- 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 +39 -11
- 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 +153 -115
- 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 -20
- 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,157 +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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
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
|
30
31
|
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
34
37
|
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
38
43
|
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
42
49
|
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
46
70
|
|
47
|
-
|
48
|
-
|
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
|
49
76
|
end
|
50
77
|
|
51
|
-
it "
|
52
|
-
|
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
|
53
82
|
end
|
83
|
+
end
|
54
84
|
|
55
|
-
|
56
|
-
|
57
|
-
|
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'] }
|
58
91
|
|
59
|
-
|
60
|
-
|
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
|
+
)
|
61
100
|
end
|
62
101
|
|
63
|
-
it
|
64
|
-
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
|
65
105
|
end
|
66
106
|
|
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-like logger is configured' do
|
95
|
-
let(:logger) do
|
96
|
-
Class.new {
|
97
|
-
def info(msg); end
|
98
|
-
}.new
|
99
|
-
end
|
100
|
-
|
101
|
-
before do
|
102
|
-
allow(logger).to receive(:info)
|
103
|
-
end
|
104
|
-
|
105
|
-
it 'logs an error message' do
|
106
|
-
expect(subject.valid?).to be false
|
107
|
-
expect(logger).to have_received(:info).with('Unable to find service provider for issuer ')
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
context 'a logger lambda is configured' do
|
112
|
-
let(:logger) { double }
|
113
|
-
|
114
|
-
before { allow(logger).to receive(:call) }
|
115
|
-
|
116
|
-
it 'logs an error message' do
|
117
|
-
expect(subject.valid?).to be false
|
118
|
-
expect(logger).to have_received(:call).with('Unable to find service provider for issuer ')
|
119
|
-
end
|
120
|
-
end
|
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)
|
121
111
|
end
|
122
112
|
end
|
123
113
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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)
|
128
118
|
|
129
|
-
|
130
|
-
expect(
|
119
|
+
expect(request.valid?).to be false
|
120
|
+
expect(request.errors).to include(:sp_not_found)
|
131
121
|
end
|
122
|
+
end
|
132
123
|
|
133
|
-
|
134
|
-
|
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)
|
135
143
|
end
|
144
|
+
end
|
136
145
|
|
137
|
-
|
138
|
-
|
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
|
+
)
|
139
159
|
end
|
140
160
|
|
141
|
-
|
142
|
-
|
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)
|
143
164
|
end
|
144
165
|
|
145
|
-
it "
|
146
|
-
|
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)
|
147
171
|
end
|
172
|
+
end
|
148
173
|
|
149
|
-
|
150
|
-
|
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)
|
151
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)
|
152
190
|
|
153
|
-
|
154
|
-
expect(
|
191
|
+
expect(request.valid?).to be false
|
192
|
+
expect(request.errors).to include(:invalid_embedded_signature)
|
155
193
|
end
|
156
194
|
end
|
157
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,41 +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,
|
44
|
-
assertions_signed: true)
|
49
|
+
def add_securty_options(settings, options = default_sp_security_options)
|
45
50
|
# Security section
|
46
51
|
settings.idp_cert = SamlIdp::Default::X509_CERTIFICATE
|
47
52
|
# Signed embedded singature
|
48
|
-
settings.security[:authn_requests_signed] = authn_requests_signed
|
49
|
-
settings.security[:embed_sign] = embed_sign
|
50
|
-
settings.security[:logout_requests_signed] = logout_requests_signed
|
51
|
-
settings.security[:logout_responses_signed] = logout_responses_signed
|
52
|
-
settings.security[:metadata_signed] = digest_method
|
53
|
-
settings.security[:digest_method] = digest_method
|
54
|
-
settings.security[:signature_method] = signature_method
|
55
|
-
settings.security[:want_assertions_signed] = assertions_signed
|
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]
|
56
61
|
settings.private_key = sp_pv_key
|
57
62
|
settings.certificate = sp_x509_cert
|
58
63
|
end
|
@@ -84,16 +89,51 @@ module SamlRequestMacros
|
|
84
89
|
response_hosts: [URI(saml_acs_url).host],
|
85
90
|
acs_url: saml_acs_url,
|
86
91
|
cert: sp_x509_cert,
|
87
|
-
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'
|
88
94
|
}
|
89
95
|
}
|
90
96
|
end
|
91
97
|
end
|
92
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
|
+
|
93
110
|
def print_pretty_xml(xml_string)
|
94
111
|
doc = REXML::Document.new xml_string
|
95
112
|
outbuf = ""
|
96
113
|
doc.write(outbuf, 1)
|
97
114
|
puts outbuf
|
98
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
|
99
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
|
|