saml_idp 0.7.2 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/Gemfile +1 -1
- data/README.md +59 -52
- data/lib/saml_idp/assertion_builder.rb +28 -3
- data/lib/saml_idp/configurator.rb +7 -1
- data/lib/saml_idp/controller.rb +21 -13
- data/lib/saml_idp/encryptor.rb +0 -1
- data/lib/saml_idp/fingerprint.rb +19 -0
- data/lib/saml_idp/incoming_metadata.rb +22 -1
- data/lib/saml_idp/metadata_builder.rb +23 -8
- data/lib/saml_idp/persisted_metadata.rb +4 -0
- data/lib/saml_idp/request.rb +26 -6
- data/lib/saml_idp/response_builder.rb +26 -6
- data/lib/saml_idp/saml_response.rb +62 -28
- data/lib/saml_idp/service_provider.rb +15 -6
- data/lib/saml_idp/signable.rb +1 -2
- data/lib/saml_idp/version.rb +1 -1
- data/lib/saml_idp/xml_security.rb +1 -1
- data/lib/saml_idp.rb +2 -1
- data/saml_idp.gemspec +45 -42
- data/spec/acceptance/idp_controller_spec.rb +5 -4
- data/spec/lib/saml_idp/algorithmable_spec.rb +6 -6
- data/spec/lib/saml_idp/assertion_builder_spec.rb +151 -8
- data/spec/lib/saml_idp/attribute_decorator_spec.rb +8 -8
- data/spec/lib/saml_idp/configurator_spec.rb +9 -7
- data/spec/lib/saml_idp/controller_spec.rb +53 -20
- data/spec/lib/saml_idp/encryptor_spec.rb +4 -4
- data/spec/lib/saml_idp/fingerprint_spec.rb +14 -0
- data/spec/lib/saml_idp/incoming_metadata_spec.rb +60 -0
- data/spec/lib/saml_idp/metadata_builder_spec.rb +30 -17
- data/spec/lib/saml_idp/name_id_formatter_spec.rb +3 -3
- data/spec/lib/saml_idp/request_spec.rb +78 -27
- data/spec/lib/saml_idp/response_builder_spec.rb +5 -3
- data/spec/lib/saml_idp/saml_response_spec.rb +127 -12
- data/spec/lib/saml_idp/service_provider_spec.rb +2 -2
- data/spec/lib/saml_idp/signable_spec.rb +1 -1
- data/spec/lib/saml_idp/signature_builder_spec.rb +2 -2
- data/spec/lib/saml_idp/signed_info_builder_spec.rb +3 -3
- data/spec/rails_app/app/controllers/saml_controller.rb +1 -1
- data/spec/rails_app/app/controllers/saml_idp_controller.rb +55 -3
- data/{app → spec/rails_app/app}/views/saml_idp/idp/new.html.erb +1 -5
- data/{app → spec/rails_app/app}/views/saml_idp/idp/saml_post.html.erb +1 -1
- data/spec/rails_app/config/application.rb +1 -6
- data/spec/rails_app/config/boot.rb +1 -1
- data/spec/rails_app/config/environments/development.rb +2 -5
- data/spec/rails_app/config/environments/production.rb +1 -0
- data/spec/rails_app/config/environments/test.rb +1 -0
- data/spec/spec_helper.rb +23 -1
- data/spec/support/certificates/sp_cert_req.csr +12 -0
- data/spec/support/certificates/sp_private_key.pem +16 -0
- data/spec/support/certificates/sp_x509_cert.crt +18 -0
- data/spec/support/saml_request_macros.rb +66 -4
- data/spec/support/security_helpers.rb +10 -0
- data/spec/xml_security_spec.rb +12 -12
- metadata +135 -81
- data/app/controllers/saml_idp/idp_controller.rb +0 -59
- data/spec/lib/saml_idp/.assertion_builder_spec.rb.swp +0 -0
@@ -2,18 +2,39 @@ require 'spec_helper'
|
|
2
2
|
module SamlIdp
|
3
3
|
describe MetadataBuilder do
|
4
4
|
it "has a valid fresh" do
|
5
|
-
subject.fresh.
|
5
|
+
expect(subject.fresh).to_not be_empty
|
6
6
|
end
|
7
7
|
|
8
8
|
it "signs valid xml" do
|
9
|
-
Saml::XML::Document.parse(subject.signed).valid_signature?(Default::FINGERPRINT).
|
9
|
+
expect(Saml::XML::Document.parse(subject.signed).valid_signature?(Default::FINGERPRINT)).to be_truthy
|
10
10
|
end
|
11
11
|
|
12
12
|
it "includes logout element" do
|
13
13
|
subject.configurator.single_logout_service_post_location = 'https://example.com/saml/logout'
|
14
|
-
subject.
|
15
|
-
|
16
|
-
)
|
14
|
+
subject.configurator.single_logout_service_redirect_location = 'https://example.com/saml/logout'
|
15
|
+
expect(subject.fresh).to match('<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://example.com/saml/logout"/>')
|
16
|
+
expect(subject.fresh).to match('<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://example.com/saml/logout"/>')
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'will not includes empty logout endpoint' do
|
20
|
+
subject.configurator.single_logout_service_post_location = ''
|
21
|
+
subject.configurator.single_logout_service_redirect_location = nil
|
22
|
+
expect(subject.fresh).not_to match('<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"')
|
23
|
+
expect(subject.fresh).not_to match('<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"')
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'will includes sso element' do
|
27
|
+
subject.configurator.single_service_post_location = 'https://example.com/saml/sso'
|
28
|
+
subject.configurator.single_service_redirect_location = 'https://example.com/saml/sso'
|
29
|
+
expect(subject.fresh).to match('<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://example.com/saml/sso"/>')
|
30
|
+
expect(subject.fresh).to match('<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://example.com/saml/sso"/>')
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'will not includes empty sso element' do
|
34
|
+
subject.configurator.single_service_post_location = ''
|
35
|
+
subject.configurator.single_service_redirect_location = nil
|
36
|
+
expect(subject.fresh).not_to match('<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"')
|
37
|
+
expect(subject.fresh).not_to match('<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"')
|
17
38
|
end
|
18
39
|
|
19
40
|
context "technical contact" do
|
@@ -32,31 +53,23 @@ module SamlIdp
|
|
32
53
|
subject.configurator.technical_contact.telephone = "1-800-555-5555"
|
33
54
|
subject.configurator.technical_contact.email_address = "acme@example.com"
|
34
55
|
|
35
|
-
subject.fresh.
|
36
|
-
'<ContactPerson contactType="technical"><Company>ACME Corporation</Company><GivenName>Road</GivenName><SurName>Runner</SurName><EmailAddress>mailto:acme@example.com</EmailAddress><TelephoneNumber>1-800-555-5555</TelephoneNumber></ContactPerson>'
|
37
|
-
)
|
56
|
+
expect(subject.fresh).to match('<ContactPerson contactType="technical"><Company>ACME Corporation</Company><GivenName>Road</GivenName><SurName>Runner</SurName><EmailAddress>mailto:acme@example.com</EmailAddress><TelephoneNumber>1-800-555-5555</TelephoneNumber></ContactPerson>')
|
38
57
|
end
|
39
58
|
|
40
59
|
it "no fields" do
|
41
|
-
subject.fresh.
|
42
|
-
'<ContactPerson contactType="technical"></ContactPerson>'
|
43
|
-
)
|
60
|
+
expect(subject.fresh).to match('<ContactPerson contactType="technical"></ContactPerson>')
|
44
61
|
end
|
45
62
|
|
46
63
|
it "just email" do
|
47
64
|
subject.configurator.technical_contact.email_address = "acme@example.com"
|
48
|
-
subject.fresh.
|
49
|
-
'<ContactPerson contactType="technical"><EmailAddress>mailto:acme@example.com</EmailAddress></ContactPerson>'
|
50
|
-
)
|
65
|
+
expect(subject.fresh).to match('<ContactPerson contactType="technical"><EmailAddress>mailto:acme@example.com</EmailAddress></ContactPerson>')
|
51
66
|
end
|
52
67
|
|
53
68
|
end
|
54
69
|
|
55
70
|
it "includes logout element as HTTP Redirect" do
|
56
71
|
subject.configurator.single_logout_service_redirect_location = 'https://example.com/saml/logout'
|
57
|
-
subject.fresh.
|
58
|
-
'<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://example.com/saml/logout"/>'
|
59
|
-
)
|
72
|
+
expect(subject.fresh).to match('<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://example.com/saml/logout"/>')
|
60
73
|
end
|
61
74
|
end
|
62
75
|
end
|
@@ -7,7 +7,7 @@ module SamlIdp
|
|
7
7
|
let(:list) { { email_address: ->() { "foo@example.com" } } }
|
8
8
|
|
9
9
|
it "has a valid all" do
|
10
|
-
subject.all.
|
10
|
+
expect(subject.all).to eq ["urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress"]
|
11
11
|
end
|
12
12
|
|
13
13
|
end
|
@@ -21,7 +21,7 @@ module SamlIdp
|
|
21
21
|
}
|
22
22
|
|
23
23
|
it "has a valid all" do
|
24
|
-
subject.all.
|
24
|
+
expect(subject.all).to eq [
|
25
25
|
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
26
26
|
"urn:oasis:names:tc:SAML:2.0:nameid-format:undefined",
|
27
27
|
]
|
@@ -32,7 +32,7 @@ module SamlIdp
|
|
32
32
|
let(:list) { [:email_address, :undefined] }
|
33
33
|
|
34
34
|
it "has a valid all" do
|
35
|
-
subject.all.
|
35
|
+
expect(subject.all).to eq [
|
36
36
|
"urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress",
|
37
37
|
"urn:oasis:names:tc:SAML:2.0:nameid-format:undefined",
|
38
38
|
]
|
@@ -1,7 +1,10 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
module SamlIdp
|
3
3
|
describe Request do
|
4
|
-
let(:
|
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
|
5
8
|
|
6
9
|
describe "deflated request" do
|
7
10
|
let(:deflated_request) { Base64.encode64(Zlib::Deflate.deflate(raw_authn_request, 9)[2..-5]) }
|
@@ -9,12 +12,12 @@ module SamlIdp
|
|
9
12
|
subject { described_class.from_deflated_request deflated_request }
|
10
13
|
|
11
14
|
it "inflates" do
|
12
|
-
subject.request_id.
|
15
|
+
expect(subject.request_id).to eq("_af43d1a0-e111-0130-661a-3c0754403fdb")
|
13
16
|
end
|
14
17
|
|
15
18
|
it "handles invalid SAML" do
|
16
19
|
req = described_class.from_deflated_request "bang!"
|
17
|
-
req.valid
|
20
|
+
expect(req.valid?).to eq(false)
|
18
21
|
end
|
19
22
|
end
|
20
23
|
|
@@ -22,51 +25,99 @@ module SamlIdp
|
|
22
25
|
subject { described_class.new raw_authn_request }
|
23
26
|
|
24
27
|
it "has a valid request_id" do
|
25
|
-
subject.request_id.
|
28
|
+
expect(subject.request_id).to eq("_af43d1a0-e111-0130-661a-3c0754403fdb")
|
26
29
|
end
|
27
30
|
|
28
31
|
it "has a valid acs_url" do
|
29
|
-
subject.acs_url.
|
32
|
+
expect(subject.acs_url).to eq("http://localhost:3000/saml/consume")
|
30
33
|
end
|
31
34
|
|
32
35
|
it "has a valid service_provider" do
|
33
|
-
subject.service_provider.
|
36
|
+
expect(subject.service_provider).to be_a ServiceProvider
|
34
37
|
end
|
35
38
|
|
36
39
|
it "has a valid service_provider" do
|
37
|
-
subject.service_provider.
|
40
|
+
expect(subject.service_provider).to be_truthy
|
38
41
|
end
|
39
42
|
|
40
43
|
it "has a valid issuer" do
|
41
|
-
subject.issuer.
|
44
|
+
expect(subject.issuer).to eq("localhost:3000")
|
42
45
|
end
|
43
46
|
|
44
47
|
it "has a valid valid_signature" do
|
45
|
-
subject.valid_signature
|
48
|
+
expect(subject.valid_signature?).to be_truthy
|
46
49
|
end
|
47
50
|
|
48
51
|
it "should return acs_url for response_url" do
|
49
|
-
subject.response_url.
|
52
|
+
expect(subject.response_url).to eq(subject.acs_url)
|
50
53
|
end
|
51
54
|
|
52
55
|
it "is a authn request" do
|
53
|
-
subject.authn_request
|
56
|
+
expect(subject.authn_request?).to eq(true)
|
54
57
|
end
|
55
58
|
|
56
59
|
it "fetches internal request" do
|
57
|
-
subject.request['ID'].
|
60
|
+
expect(subject.request['ID']).to eq(subject.request_id)
|
58
61
|
end
|
59
62
|
|
60
|
-
it
|
61
|
-
subject.requested_authn_context.
|
63
|
+
it 'has a valid authn context' do
|
64
|
+
expect(subject.requested_authn_context).to eq('urn:oasis:names:tc:SAML:2.0:ac:classes:Password')
|
62
65
|
end
|
63
66
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
67
|
+
context 'the issuer is empty' do
|
68
|
+
let(:issuer) { nil }
|
69
|
+
let(:logger) { ->(msg) { puts msg } }
|
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
|
70
121
|
end
|
71
122
|
end
|
72
123
|
|
@@ -76,31 +127,31 @@ module SamlIdp
|
|
76
127
|
subject { described_class.new raw_logout_request }
|
77
128
|
|
78
129
|
it "has a valid request_id" do
|
79
|
-
subject.request_id.
|
130
|
+
expect(subject.request_id).to eq('_some_response_id')
|
80
131
|
end
|
81
132
|
|
82
133
|
it "should be flagged as a logout_request" do
|
83
|
-
subject.logout_request
|
134
|
+
expect(subject.logout_request?).to eq(true)
|
84
135
|
end
|
85
136
|
|
86
137
|
it "should have a valid name_id" do
|
87
|
-
subject.name_id.
|
138
|
+
expect(subject.name_id).to eq('some_name_id')
|
88
139
|
end
|
89
140
|
|
90
141
|
it "should have a session index" do
|
91
|
-
subject.session_index.
|
142
|
+
expect(subject.session_index).to eq('abc123index')
|
92
143
|
end
|
93
144
|
|
94
145
|
it "should have a valid issuer" do
|
95
|
-
subject.issuer.
|
146
|
+
expect(subject.issuer).to eq('http://example.com')
|
96
147
|
end
|
97
148
|
|
98
149
|
it "fetches internal request" do
|
99
|
-
subject.request['ID'].
|
150
|
+
expect(subject.request['ID']).to eq(subject.request_id)
|
100
151
|
end
|
101
152
|
|
102
153
|
it "should return logout_url for response_url" do
|
103
|
-
subject.response_url.
|
154
|
+
expect(subject.response_url).to eq(subject.logout_url)
|
104
155
|
end
|
105
156
|
end
|
106
157
|
end
|
@@ -6,12 +6,14 @@ module SamlIdp
|
|
6
6
|
let(:saml_acs_url) { "http://sportngin.com" }
|
7
7
|
let(:saml_request_id) { "134" }
|
8
8
|
let(:assertion_and_signature) { "<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"_abc\" IssueInstant=\"2013-07-31T05:00:00Z\" Version=\"2.0\"><Issuer>http://sportngin.com</Issuer><signature>stuff</signature><Subject><NameID Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\">jon.phenow@sportngin.com</NameID><SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><SubjectConfirmationData InResponseTo=\"123\" NotOnOrAfter=\"2013-07-31T05:03:00Z\" Recipient=\"http://saml.acs.url\"/></SubjectConfirmation></Subject><Conditions NotBefore=\"2013-07-31T04:59:55Z\" NotOnOrAfter=\"2013-07-31T06:00:00Z\"><AudienceRestriction><Audience>http://example.com</Audience></AudienceRestriction></Conditions><AttributeStatement><Attribute Name=\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress\"><AttributeValue>jon.phenow@sportngin.com</AttributeValue></Attribute></AttributeStatement><AuthnStatment AuthnInstant=\"2013-07-31T05:00:00Z\" SessionIndex=\"_abc\"><AuthnContext><AuthnContextClassRef>urn:federation:authentication:windows</AuthnContextClassRef></AuthnContext></AuthnStatment></Assertion>" }
|
9
|
+
let(:algorithm) { :sha256 }
|
9
10
|
subject { described_class.new(
|
10
11
|
response_id,
|
11
12
|
issuer_uri,
|
12
13
|
saml_acs_url,
|
13
14
|
saml_request_id,
|
14
|
-
assertion_and_signature
|
15
|
+
assertion_and_signature,
|
16
|
+
algorithm
|
15
17
|
) }
|
16
18
|
|
17
19
|
before do
|
@@ -25,7 +27,7 @@ module SamlIdp
|
|
25
27
|
|
26
28
|
it "builds a legit raw XML file" do
|
27
29
|
Timecop.travel(Time.zone.local(2010, 6, 1, 13, 0, 0)) do
|
28
|
-
subject.raw.
|
30
|
+
expect(subject.raw).to eq("<samlp:Response ID=\"_abc\" Version=\"2.0\" IssueInstant=\"2010-06-01T13:00:00Z\" Destination=\"http://sportngin.com\" Consent=\"urn:oasis:names:tc:SAML:2.0:consent:unspecified\" InResponseTo=\"134\" xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://example.com</Issuer><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/></samlp:Status><Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"_abc\" IssueInstant=\"2013-07-31T05:00:00Z\" Version=\"2.0\"><Issuer>http://sportngin.com</Issuer><signature>stuff</signature><Subject><NameID Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\">jon.phenow@sportngin.com</NameID><SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><SubjectConfirmationData InResponseTo=\"123\" NotOnOrAfter=\"2013-07-31T05:03:00Z\" Recipient=\"http://saml.acs.url\"/></SubjectConfirmation></Subject><Conditions NotBefore=\"2013-07-31T04:59:55Z\" NotOnOrAfter=\"2013-07-31T06:00:00Z\"><AudienceRestriction><Audience>http://example.com</Audience></AudienceRestriction></Conditions><AttributeStatement><Attribute Name=\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress\"><AttributeValue>jon.phenow@sportngin.com</AttributeValue></Attribute></AttributeStatement><AuthnStatment AuthnInstant=\"2013-07-31T05:00:00Z\" SessionIndex=\"_abc\"><AuthnContext><AuthnContextClassRef>urn:federation:authentication:windows</AuthnContextClassRef></AuthnContext></AuthnStatment></Assertion></samlp:Response>")
|
29
31
|
end
|
30
32
|
end
|
31
33
|
|
@@ -34,7 +36,7 @@ module SamlIdp
|
|
34
36
|
|
35
37
|
it "builds a legit raw XML file without a request ID" do
|
36
38
|
Timecop.travel(Time.zone.local(2010, 6, 1, 13, 0, 0)) do
|
37
|
-
subject.raw.
|
39
|
+
expect(subject.raw).to eq("<samlp:Response ID=\"_abc\" Version=\"2.0\" IssueInstant=\"2010-06-01T13:00:00Z\" Destination=\"http://sportngin.com\" Consent=\"urn:oasis:names:tc:SAML:2.0:consent:unspecified\" xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://example.com</Issuer><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/></samlp:Status><Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"_abc\" IssueInstant=\"2013-07-31T05:00:00Z\" Version=\"2.0\"><Issuer>http://sportngin.com</Issuer><signature>stuff</signature><Subject><NameID Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\">jon.phenow@sportngin.com</NameID><SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><SubjectConfirmationData InResponseTo=\"123\" NotOnOrAfter=\"2013-07-31T05:03:00Z\" Recipient=\"http://saml.acs.url\"/></SubjectConfirmation></Subject><Conditions NotBefore=\"2013-07-31T04:59:55Z\" NotOnOrAfter=\"2013-07-31T06:00:00Z\"><AudienceRestriction><Audience>http://example.com</Audience></AudienceRestriction></Conditions><AttributeStatement><Attribute Name=\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress\"><AttributeValue>jon.phenow@sportngin.com</AttributeValue></Attribute></AttributeStatement><AuthnStatment AuthnInstant=\"2013-07-31T05:00:00Z\" SessionIndex=\"_abc\"><AuthnContext><AuthnContextClassRef>urn:federation:authentication:windows</AuthnContextClassRef></AuthnContext></AuthnStatment></Assertion></samlp:Response>")
|
38
40
|
end
|
39
41
|
end
|
40
42
|
end
|
@@ -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
|
|
@@ -63,23 +77,124 @@ module SamlIdp
|
|
63
77
|
end
|
64
78
|
|
65
79
|
it "has a valid build" do
|
66
|
-
subject.build.
|
80
|
+
expect(subject.build).to be_present
|
81
|
+
end
|
82
|
+
|
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
|
67
174
|
end
|
68
175
|
|
69
|
-
it "
|
70
|
-
|
71
|
-
|
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(
|
76
|
-
|
77
|
-
|
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)
|
192
|
+
expect(saml_resp.is_valid?).to eq(true)
|
78
193
|
end
|
79
194
|
|
80
195
|
it "sets session expiration" do
|
81
196
|
saml_resp = OneLogin::RubySaml::Response.new(subject.build)
|
82
|
-
saml_resp.session_expires_at.
|
197
|
+
expect(saml_resp.session_expires_at).to eq Time.local(1990, "jan", 2).iso8601
|
83
198
|
end
|
84
199
|
|
85
200
|
context "session expiration is set to 0" do
|
@@ -89,14 +204,14 @@ module SamlIdp
|
|
89
204
|
resp_settings = saml_settings(saml_acs_url)
|
90
205
|
resp_settings.issuer = audience_uri
|
91
206
|
saml_resp = OneLogin::RubySaml::Response.new(subject.build, settings: resp_settings)
|
92
|
-
saml_resp.is_valid
|
207
|
+
expect(saml_resp.is_valid?).to eq(true)
|
93
208
|
end
|
94
209
|
|
95
210
|
it "doesn't set a session expiration" do
|
96
211
|
resp_settings = saml_settings(saml_acs_url)
|
97
212
|
resp_settings.issuer = audience_uri
|
98
213
|
saml_resp = OneLogin::RubySaml::Response.new(subject.build, settings: resp_settings)
|
99
|
-
saml_resp.session_expires_at.
|
214
|
+
expect(saml_resp.session_expires_at).to be_nil
|
100
215
|
end
|
101
216
|
end
|
102
217
|
end
|
@@ -14,11 +14,11 @@ module SamlIdp
|
|
14
14
|
let(:metadata_url) { "http://localhost:3000/metadata" }
|
15
15
|
|
16
16
|
it "has a valid fingerprint" do
|
17
|
-
subject.fingerprint.
|
17
|
+
expect(subject.fingerprint).to eq(fingerprint)
|
18
18
|
end
|
19
19
|
|
20
20
|
it "has a valid metadata_url" do
|
21
|
-
subject.metadata_url.
|
21
|
+
expect(subject.metadata_url).to eq(metadata_url)
|
22
22
|
end
|
23
23
|
|
24
24
|
it { should be_valid }
|
@@ -9,11 +9,11 @@ module SamlIdp
|
|
9
9
|
) }
|
10
10
|
|
11
11
|
before do
|
12
|
-
Time.
|
12
|
+
allow(Time).to receive(:now).and_return Time.parse("Jul 31 2013")
|
13
13
|
end
|
14
14
|
|
15
15
|
it "builds a legit raw XML file" do
|
16
|
-
subject.raw.
|
16
|
+
expect(subject.raw).to eq("<ds:Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"><ds:SignedInfo xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"><ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/><ds:SignatureMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#rsa-sha256\"/><ds:Reference URI=\"#_abc\"><ds:Transforms><ds:Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature\"/><ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/></ds:Transforms><ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha256\"/><ds:DigestValue>em8csGAWynywpe8S4nN64o56/4DosXi2XWMY6RJ6YfA=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>jvEbD/rsiPKmoXy7Lhm+FGn88NPGlap4EcPZ2fvjBnk03YESs87FXAIiZZEzN5xq4sBZksUmZe2bV3rrr9sxQNgQawmrrvr66ot7cJiv0ETFArr6kQIZaR5g/V0M4ydxvrfefp6cQVI0hXvmxi830pq0tISiO4J7tyBNX/kvhZk=</ds:SignatureValue><KeyInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><ds:X509Data><ds:X509Certificate>MIIDqzCCAxSgAwIBAgIBATANBgkqhkiG9w0BAQsFADCBhjELMAkGA1UEBhMCQVUxDDAKBgNVBAgTA05TVzEPMA0GA1UEBxMGU3lkbmV5MQwwCgYDVQQKDANQSVQxCTAHBgNVBAsMADEYMBYGA1UEAwwPbGF3cmVuY2VwaXQuY29tMSUwIwYJKoZIhvcNAQkBDBZsYXdyZW5jZS5waXRAZ21haWwuY29tMB4XDTEyMDQyODAyMjIyOFoXDTMyMDQyMzAyMjIyOFowgYYxCzAJBgNVBAYTAkFVMQwwCgYDVQQIEwNOU1cxDzANBgNVBAcTBlN5ZG5leTEMMAoGA1UECgwDUElUMQkwBwYDVQQLDAAxGDAWBgNVBAMMD2xhd3JlbmNlcGl0LmNvbTElMCMGCSqGSIb3DQEJAQwWbGF3cmVuY2UucGl0QGdtYWlsLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuBywPNlC1FopGLYfF96SotiK8Nj6/nW084O4omRMifzy7x955RLEy673q2aiJNB3LvE6Xvkt9cGtxtNoOXw1g2UvHKpldQbr6bOEjLNeDNW7j0ob+JrRvAUOK9CRgdyw5MC6lwqVQQ5C1DnaT/2fSBFjasBFTR24dEpfTy8HfKECAwEAAaOCASUwggEhMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgUgMB0GA1UdDgQWBBQNBGmmt3ytKpcJaBaYNbnyU2xkazATBgNVHSUEDDAKBggrBgEFBQcDATAdBglghkgBhvhCAQ0EEBYOVGVzdCBYNTA5IGNlcnQwgbMGA1UdIwSBqzCBqIAUDQRpprd8rSqXCWgWmDW58lNsZGuhgYykgYkwgYYxCzAJBgNVBAYTAkFVMQwwCgYDVQQIEwNOU1cxDzANBgNVBAcTBlN5ZG5leTEMMAoGA1UECgwDUElUMQkwBwYDVQQLDAAxGDAWBgNVBAMMD2xhd3JlbmNlcGl0LmNvbTElMCMGCSqGSIb3DQEJAQwWbGF3cmVuY2UucGl0QGdtYWlsLmNvbYIBATANBgkqhkiG9w0BAQsFAAOBgQAEcVUPBX7uZmzqZJfy+tUPOT5ImNQj8VE2lerhnFjnGPHmHIqhpzgnwHQujJfs/a309Wm5qwcCaC1eO5cWjcG0x3OjdllsgYDatl5GAumtBx8J3NhWRqNUgitCIkQlxHIwUfgQaCushYgDDL5YbIQa++egCgpIZ+T0Dj5oRew//A==</ds:X509Certificate></ds:X509Data></KeyInfo></ds:Signature>")
|
17
17
|
end
|
18
18
|
end
|
19
19
|
end
|
@@ -11,15 +11,15 @@ module SamlIdp
|
|
11
11
|
) }
|
12
12
|
|
13
13
|
before do
|
14
|
-
Time.
|
14
|
+
allow(Time).to receive(:now).and_return Time.parse("Jul 31 2013")
|
15
15
|
end
|
16
16
|
|
17
17
|
it "builds a legit raw XML file" do
|
18
|
-
subject.raw.
|
18
|
+
expect(subject.raw).to eq("<ds:SignedInfo xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"><ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"></ds:CanonicalizationMethod><ds:SignatureMethod Algorithm=\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256\"></ds:SignatureMethod><ds:Reference URI=\"#_abc\"><ds:Transforms><ds:Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature\"></ds:Transform><ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"></ds:DigestMethod><ds:DigestValue>em8csGAWynywpe8S4nN64o56/4DosXi2XWMY6RJ6YfA=</ds:DigestValue></ds:Reference></ds:SignedInfo>")
|
19
19
|
end
|
20
20
|
|
21
21
|
it "builds a legit digest of the XML file" do
|
22
|
-
subject.signed.
|
22
|
+
expect(subject.signed).to eq("hKLeWLRgatHcV6N5Fc8aKveqNp6Y/J4m2WSYp0awGFtsCTa/2nab32wI3du+3kuuIy59EDKeUhHVxEfyhoHUo6xTZuO2N7XcTpSonuZ/CB3WjozC2Q/9elss3z1rOC3154v5pW4puirLPRoG+Pwi8SmptxNRHczr6NvmfYmmGfo=")
|
23
23
|
end
|
24
24
|
end
|
25
25
|
end
|