ruby-saml-mod 0.1.30 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/onelogin/saml.rb +3 -2
- data/lib/onelogin/saml/auth_request.rb +37 -43
- data/lib/onelogin/saml/base_assertion.rb +159 -0
- data/lib/onelogin/saml/logout_request.rb +44 -0
- data/lib/onelogin/saml/logout_response.rb +24 -25
- data/lib/onelogin/saml/response.rb +2 -4
- data/lib/onelogin/saml/settings.rb +23 -17
- data/spec/base_assertion_spec.rb +5 -0
- data/spec/fixtures/logout_request.xml +11 -0
- data/spec/fixtures/logout_response.xml +13 -0
- data/spec/fixtures/test1-cert.pem +21 -0
- data/spec/fixtures/test1-key.pem +15 -0
- data/spec/fixtures/test1-response.xml +62 -0
- data/spec/fixtures/test2-response.xml +70 -0
- data/spec/fixtures/test3-response.xml +9 -0
- data/spec/fixtures/test4-response.xml +57 -0
- data/spec/fixtures/test5-response.xml +48 -0
- data/spec/fixtures/test6-response.xml +9 -0
- data/spec/fixtures/wrong-key.pem +15 -0
- data/spec/fixtures/xml_signature_wrapping_attack_duplicate_ids.xml +11 -0
- data/spec/fixtures/xml_signature_wrapping_attack_response_attributes.xml +45 -0
- data/spec/fixtures/xml_signature_wrapping_attack_response_nameid.xml +44 -0
- data/spec/logout_request_spec.rb +89 -0
- data/spec/logout_response_spec.rb +76 -0
- data/spec/meta_data_spec.rb +39 -0
- data/spec/response_spec.rb +193 -0
- data/spec/spec_helper.rb +33 -0
- data/spec/support/test_server.rb +73 -0
- metadata +77 -10
- data/LICENSE +0 -19
- data/README +0 -7
- data/lib/onelogin/saml/log_out_request.rb +0 -54
- data/ruby-saml-mod.gemspec +0 -33
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
def verify_query_string_signature(settings, forward_url)
|
4
|
+
url = URI.parse(forward_url)
|
5
|
+
signed_data, signature = url.query.split('&Signature=')
|
6
|
+
cert = OpenSSL::X509::Certificate.new(File.read(settings.xmlsec_certificate))
|
7
|
+
cert.public_key.verify(OpenSSL::Digest::SHA1.new, Base64.decode64(CGI.unescape(signature)), signed_data)
|
8
|
+
end
|
9
|
+
|
10
|
+
# see http://stackoverflow.com/questions/1361892/how-to-decompress-gzip-string-in-ruby
|
11
|
+
def inflate(string)
|
12
|
+
zstream = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
13
|
+
buf = zstream.inflate(string)
|
14
|
+
zstream.finish
|
15
|
+
zstream.close
|
16
|
+
buf
|
17
|
+
end
|
18
|
+
|
19
|
+
describe Onelogin::Saml::LogoutRequest do
|
20
|
+
let(:settings) do
|
21
|
+
Onelogin::Saml::Settings.new(
|
22
|
+
:xmlsec_certificate => fixture_path("test1-cert.pem"),
|
23
|
+
:xmlsec_privatekey => fixture_path("test1-key.pem"),
|
24
|
+
:idp_slo_target_url => "http://idp.example.com/saml2",
|
25
|
+
:idp_cert_fingerprint => 'def18dbed547cdf3d52b627f41637c443045fe33',
|
26
|
+
:name_identifier_format => Onelogin::Saml::NameIdentifiers::UNSPECIFIED
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
let(:name_qualifier) { 'foo' }
|
31
|
+
let(:name_id) { 'bar'}
|
32
|
+
let(:session_index) { 'baz' }
|
33
|
+
|
34
|
+
let(:logout_request) do
|
35
|
+
Onelogin::Saml::LogoutRequest::generate(
|
36
|
+
name_qualifier,
|
37
|
+
name_id,
|
38
|
+
session_index,
|
39
|
+
settings
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
let(:forward_url) { logout_request.forward_url }
|
44
|
+
|
45
|
+
it "includes destination in the saml:LogoutRequest attributes" do
|
46
|
+
logout_xml = LibXML::XML::Document.string(logout_request.xml)
|
47
|
+
logout_xml.find_first('/samlp:LogoutRequest', Onelogin::NAMESPACES).attributes['Destination'].should == "http://idp.example.com/saml2"
|
48
|
+
end
|
49
|
+
|
50
|
+
it "properly sets the Format attribute NameID based on settings" do
|
51
|
+
logout_xml = LibXML::XML::Document.string(logout_request.xml)
|
52
|
+
logout_xml.find_first('/samlp:LogoutRequest/saml:NameID', Onelogin::NAMESPACES).attributes['Format'].should == Onelogin::Saml::NameIdentifiers::UNSPECIFIED
|
53
|
+
end
|
54
|
+
|
55
|
+
it "does not include the signature in the request xml" do
|
56
|
+
logout_xml = LibXML::XML::Document.string(logout_request.xml)
|
57
|
+
logout_xml.find_first('/samlp:LogoutRequest/ds:Signature', Onelogin::NAMESPACES).should be_nil
|
58
|
+
end
|
59
|
+
|
60
|
+
it "can sign the generated query string" do
|
61
|
+
expect(verify_query_string_signature(settings, forward_url)).to be_true
|
62
|
+
end
|
63
|
+
|
64
|
+
it "properly signs when the IDP URL already contains a query string" do
|
65
|
+
settings = Onelogin::Saml::Settings.new(
|
66
|
+
:xmlsec_certificate => fixture_path("test1-cert.pem"),
|
67
|
+
:xmlsec_privatekey => fixture_path("test1-key.pem"),
|
68
|
+
:idp_slo_target_url => "http://idp.example.com/saml2?existing=param",
|
69
|
+
:idp_cert_fingerprint => 'def18dbed547cdf3d52b627f41637c443045fe33',
|
70
|
+
:name_identifier_format => Onelogin::Saml::NameIdentifiers::UNSPECIFIED
|
71
|
+
)
|
72
|
+
request = Onelogin::Saml::LogoutRequest.generate(name_qualifier, name_id, session_index, settings)
|
73
|
+
expect(request.forward_url).to match(%r{^http://idp.example.com/saml2\?existing=param&})
|
74
|
+
expect(verify_query_string_signature(settings, request.forward_url)).to be_true
|
75
|
+
end
|
76
|
+
|
77
|
+
it "parses a logout request" do
|
78
|
+
xml = Zlib::Deflate.deflate(File.read(fixture_path("logout_request.xml")), 9)[2..-5]
|
79
|
+
|
80
|
+
xmlb64 = Base64.encode64(xml)
|
81
|
+
settings = Onelogin::Saml::Settings.new
|
82
|
+
request = Onelogin::Saml::LogoutRequest::parse(xmlb64)
|
83
|
+
|
84
|
+
expect(request.id).to eq '_cbb63e9741259e3f1c98a1ae38ac5ac25889720b32'
|
85
|
+
expect(request.issuer).to eq 'http://saml.example.com:8080/opensso'
|
86
|
+
expect(request.name_id).to eq '_6a171f538d4f733ae95eca74ce264cfb602808c850'
|
87
|
+
expect(request.session_index).to eq '_b976de57fcf0f707de297069f33a6b0248827d96a9'
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'rexml/document'
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
describe Onelogin::Saml::LogoutResponse do
|
6
|
+
let(:id) { Onelogin::Saml::LogoutResponse.generate_unique_id(42) }
|
7
|
+
let(:issue_instant) { Onelogin::Saml::LogoutResponse.get_timestamp }
|
8
|
+
let(:in_response_to) { Onelogin::Saml::LogoutResponse.generate_unique_id(42) }
|
9
|
+
let(:idp_slo_target_url) { 'http://idp.example.com/saml2' }
|
10
|
+
let(:issuer) { 'http://idp.example.com/saml2' }
|
11
|
+
let(:session) { {} }
|
12
|
+
|
13
|
+
let(:settings) do
|
14
|
+
Onelogin::Saml::Settings.new(
|
15
|
+
idp_slo_target_url: idp_slo_target_url,
|
16
|
+
issuer: issuer
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:xml) do
|
21
|
+
allow(Onelogin::Saml::LogoutResponse).to receive(:generate_unique_id).and_return(id)
|
22
|
+
allow(Onelogin::Saml::LogoutResponse).to receive(:get_timestamp).and_return(issue_instant)
|
23
|
+
|
24
|
+
Onelogin::Saml::LogoutResponse::generate(in_response_to, settings).document
|
25
|
+
end
|
26
|
+
|
27
|
+
it "includes destination in the saml:LogoutRequest attributes" do
|
28
|
+
value = xml.find_first('/samlp:LogoutResponse', Onelogin::NAMESPACES).attributes['Destination']
|
29
|
+
expect(value).to eq "http://idp.example.com/saml2"
|
30
|
+
end
|
31
|
+
|
32
|
+
it "includes id in the saml:LogoutRequest attributes" do
|
33
|
+
value = xml.find_first('/samlp:LogoutResponse', Onelogin::NAMESPACES).attributes['ID']
|
34
|
+
expect(value).to eq id
|
35
|
+
end
|
36
|
+
|
37
|
+
it "includes issue_instant in the saml:LogoutRequest attributes" do
|
38
|
+
value = xml.find_first('/samlp:LogoutResponse', Onelogin::NAMESPACES).attributes['IssueInstant']
|
39
|
+
expect(value).to eq issue_instant
|
40
|
+
end
|
41
|
+
|
42
|
+
it "includes in_response_to in the saml:LogoutRequest attributes" do
|
43
|
+
value = xml.find_first('/samlp:LogoutResponse', Onelogin::NAMESPACES).attributes['InResponseTo']
|
44
|
+
expect(value).to eq in_response_to
|
45
|
+
end
|
46
|
+
|
47
|
+
it "includes issuer tag" do
|
48
|
+
value = xml.find_first("/samlp:LogoutResponse/saml:Issuer", Onelogin::NAMESPACES).content
|
49
|
+
expect(value).to eq issuer
|
50
|
+
end
|
51
|
+
|
52
|
+
it "includes status code tag" do
|
53
|
+
value = xml.find_first("/samlp:LogoutResponse/samlp:Status/samlp:StatusCode", Onelogin::NAMESPACES).attributes['Value']
|
54
|
+
expect(value).to eq Onelogin::Saml::StatusCodes::SUCCESS_URI
|
55
|
+
end
|
56
|
+
|
57
|
+
it "includes status message tag" do
|
58
|
+
value = xml.find_first("/samlp:LogoutResponse/samlp:Status/samlp:StatusMessage", Onelogin::NAMESPACES).content
|
59
|
+
expect(value).to eq Onelogin::Saml::LogoutResponse::STATUS_MESSAGE
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should use namespaces correctly to look up attributes" do
|
63
|
+
xml = Zlib::Deflate.deflate(File.read(fixture_path("logout_response.xml")), 9)[2..-5]
|
64
|
+
|
65
|
+
xmlb64 = Base64.encode64(xml)
|
66
|
+
settings = Onelogin::Saml::Settings.new(:idp_cert_fingerprint => 'def18dbed547cdf3d52b627f41637c443045fe33')
|
67
|
+
response = Onelogin::Saml::LogoutResponse::parse(xmlb64, settings)
|
68
|
+
|
69
|
+
expect(response.id).to eq '_cbb63e9741259e3f1c98a1ae38ac5ac25889720b32'
|
70
|
+
expect(response.issuer).to eq 'http://saml.example.com:8080/opensso'
|
71
|
+
expect(response.in_response_to).to eq "_72424ea37e28763e351189529639b9c2b150ff37e5"
|
72
|
+
expect(response.destination).to eq "http://saml.example.com:8080/opensso/SingleLogoutService"
|
73
|
+
expect(response.status_code).to eq Onelogin::Saml::StatusCodes::SUCCESS_URI
|
74
|
+
expect(response.status_message).to eq "Successfully logged out from service"
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Onelogin::Saml::MetaData do
|
4
|
+
before do
|
5
|
+
@settings = Onelogin::Saml::Settings.new(:issuer => "yourmom", :sp_slo_url => 'http://example.com/logout')
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should have correct consumer service with one endpoint" do
|
9
|
+
@settings.assertion_consumer_service_url = 'http://example.com/consume'
|
10
|
+
doc = REXML::Document.new Onelogin::Saml::MetaData.create(@settings)
|
11
|
+
service = REXML::XPath.first(doc, "//AssertionConsumerService/")
|
12
|
+
service.attributes["index"].should == "0"
|
13
|
+
service.attributes["Location"].should == 'http://example.com/consume'
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should have correct consumer service with multiple endpoints" do
|
17
|
+
@settings.assertion_consumer_service_url = ['http://example.com/consume', 'http://example.com/alt_consume']
|
18
|
+
doc = REXML::Document.new Onelogin::Saml::MetaData.create(@settings)
|
19
|
+
services = REXML::XPath.match(doc, "//AssertionConsumerService/")
|
20
|
+
services[0].attributes["index"].should == "0"
|
21
|
+
services[0].attributes["Location"].should == @settings.assertion_consumer_service_url[0]
|
22
|
+
services[1].attributes["index"].should == "1"
|
23
|
+
services[1].attributes["Location"].should == @settings.assertion_consumer_service_url[1]
|
24
|
+
end
|
25
|
+
|
26
|
+
it "publishes the public key for both encryption and signing" do
|
27
|
+
settings = Onelogin::Saml::Settings.new(
|
28
|
+
:xmlsec_certificate => fixture_path("test1-cert.pem"),
|
29
|
+
:xmlsec_privatekey => fixture_path("test1-key.pem"),
|
30
|
+
:idp_slo_target_url => "http://idp.example.com/saml2",
|
31
|
+
:idp_cert_fingerprint => 'def18dbed547cdf3d52b627f41637c443045fe33'
|
32
|
+
)
|
33
|
+
doc = REXML::Document.new Onelogin::Saml::MetaData.create(settings)
|
34
|
+
key_descriptors = REXML::XPath.match(doc, "//KeyDescriptor")
|
35
|
+
key_descriptors.should have(2).keys
|
36
|
+
key_descriptors[0].attributes["use"].should == "encryption"
|
37
|
+
key_descriptors[1].attributes["use"].should == "signing"
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Onelogin::Saml::Response do
|
4
|
+
describe "decrypting assertions" do
|
5
|
+
before :each do
|
6
|
+
@xmlb64 = Base64.encode64(File.read(fixture_path("test1-response.xml")))
|
7
|
+
@settings = Onelogin::Saml::Settings.new(
|
8
|
+
:xmlsec_certificate => fixture_path("test1-cert.pem"),
|
9
|
+
:xmlsec_privatekey => fixture_path("test1-key.pem"),
|
10
|
+
:idp_cert_fingerprint => 'def18dbed547cdf3d52b627f41637c443045fe33'
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should find the right attributes from an encrypted assertion" do
|
15
|
+
@response = Onelogin::Saml::Response.new(@xmlb64, @settings)
|
16
|
+
@response.should be_is_valid
|
17
|
+
|
18
|
+
@response.name_id.should == "zach@zwily.com"
|
19
|
+
@response.name_qualifier.should == "http://saml.example.com:8080/opensso"
|
20
|
+
@response.session_index.should == "s2c57ee92b5ca08e93d751987d591c58acc68d2501"
|
21
|
+
@response.status_code.should == "urn:oasis:names:tc:SAML:2.0:status:Success"
|
22
|
+
@response.status_message.strip.should == ""
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should not be able to decrypt without the proper key" do
|
26
|
+
@settings.xmlsec_privatekey = fixture_path("wrong-key.pem")
|
27
|
+
XMLSecurity.mute do
|
28
|
+
@response = Onelogin::Saml::Response.new(@xmlb64, @settings)
|
29
|
+
end
|
30
|
+
document = REXML::Document.new(@response.decrypted_document.to_s)
|
31
|
+
REXML::XPath.first(document, "/samlp:Response/saml:Assertion").should be_nil
|
32
|
+
@response.name_qualifier.should be_nil
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should be able to decrypt using additional private keys" do
|
36
|
+
@settings.xmlsec_privatekey = fixture_path("wrong-key.pem")
|
37
|
+
@settings.xmlsec_additional_privatekeys = [fixture_path("test1-key.pem")]
|
38
|
+
XMLSecurity.mute do
|
39
|
+
@response = Onelogin::Saml::Response.new(@xmlb64, @settings)
|
40
|
+
end
|
41
|
+
document = REXML::Document.new(@response.decrypted_document.to_s)
|
42
|
+
REXML::XPath.first(document, "/samlp:Response/saml:Assertion").should_not be_nil
|
43
|
+
REXML::XPath.first(document, "/samlp:Response/saml:Assertion/ds:Signature/ds:SignedInfo/ds:Reference/ds:DigestValue").text.should == "eMQal6uuWKMbUMbOwBfrFH90bzE="
|
44
|
+
@response.name_qualifier.should == "http://saml.example.com:8080/opensso"
|
45
|
+
@response.session_index.should == "s2c57ee92b5ca08e93d751987d591c58acc68d2501"
|
46
|
+
@response.status_code.should == "urn:oasis:names:tc:SAML:2.0:status:Success"
|
47
|
+
@response.status_message.strip.should == ""
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should not verify when XSLT transforms are being used" do
|
52
|
+
@xmlb64 = Base64.encode64(File.read(fixture_path("test4-response.xml")))
|
53
|
+
@settings = Onelogin::Saml::Settings.new(:idp_cert_fingerprint => 'bc71f7bacb36011694405dd0e2beafcc069de45f')
|
54
|
+
@response = Onelogin::Saml::Response.new(@xmlb64, @settings)
|
55
|
+
|
56
|
+
XMLSecurity.mute do
|
57
|
+
@response.should_not be_is_valid
|
58
|
+
end
|
59
|
+
|
60
|
+
TestServer.requests.should == []
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should not allow external reference URIs" do
|
64
|
+
@xmlb64 = Base64.encode64(File.read(fixture_path("test5-response.xml")))
|
65
|
+
@settings = Onelogin::Saml::Settings.new(:idp_cert_fingerprint => 'bc71f7bacb36011694405dd0e2beafcc069de45f')
|
66
|
+
@response = Onelogin::Saml::Response.new(@xmlb64, @settings)
|
67
|
+
|
68
|
+
XMLSecurity.mute do
|
69
|
+
@response.should_not be_is_valid
|
70
|
+
end
|
71
|
+
|
72
|
+
TestServer.requests.should == []
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should use namespaces correctly to look up attributes" do
|
76
|
+
@xmlb64 = Base64.encode64(File.read(fixture_path("test2-response.xml")))
|
77
|
+
@settings = Onelogin::Saml::Settings.new(:idp_cert_fingerprint => 'def18dbed547cdf3d52b627f41637c443045fe33')
|
78
|
+
@response = Onelogin::Saml::Response.new(@xmlb64)
|
79
|
+
@response.disable_signature_validation!(@settings)
|
80
|
+
@response.process(@settings)
|
81
|
+
@response.name_id.should == "zach@example.com"
|
82
|
+
@response.name_qualifier.should == "http://saml.example.com:8080/opensso"
|
83
|
+
@response.session_index.should == "s2c57ee92b5ca08e93d751987d591c58acc68d2501"
|
84
|
+
@response.status_code.should == "urn:oasis:names:tc:SAML:2.0:status:Success"
|
85
|
+
@response.saml_attributes['eduPersonAffiliation'].should == 'member'
|
86
|
+
@response.saml_attributes['eduPersonPrincipalName'].should == 'user@example.edu'
|
87
|
+
@response.status_message.should == ""
|
88
|
+
@response.fingerprint_from_idp.should == 'def18dbed547cdf3d52b627f41637c443045fe33'
|
89
|
+
@response.issuer.should == 'http://saml.example.com:8080/opensso'
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should protect against xml signature wrapping attacks targeting nameid" do
|
93
|
+
@xmlb64 = Base64.encode64(File.read(fixture_path("xml_signature_wrapping_attack_response_nameid.xml")))
|
94
|
+
@settings = Onelogin::Saml::Settings.new(:idp_cert_fingerprint => 'afe71c28ef740bc87425be13a2263d37971da1f9')
|
95
|
+
@response = Onelogin::Saml::Response.new(@xmlb64)
|
96
|
+
@response.process(@settings)
|
97
|
+
@response.should be_is_valid
|
98
|
+
@response.name_id.should == "_3b3e7714b72e29dc4290321a075fa0b73333a4f25f"
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should protect against xml signature wrapping attacks targeting attributes" do
|
102
|
+
@xmlb64 = Base64.encode64(File.read(fixture_path("xml_signature_wrapping_attack_response_attributes.xml")))
|
103
|
+
@settings = Onelogin::Saml::Settings.new(:idp_cert_fingerprint => 'afe71c28ef740bc87425be13a2263d37971da1f9')
|
104
|
+
@response = Onelogin::Saml::Response.new(@xmlb64)
|
105
|
+
@response.process(@settings)
|
106
|
+
@response.should be_is_valid
|
107
|
+
@response.saml_attributes['eduPersonAffiliation'].should == 'member'
|
108
|
+
@response.saml_attributes['eduPersonPrincipalName'].should == 'student@example.edu'
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should protect against xml signature wrapping attacks with duplicate IDs" do
|
112
|
+
@xmlb64 = Base64.encode64(File.read(fixture_path('xml_signature_wrapping_attack_duplicate_ids.xml')))
|
113
|
+
@settings = Onelogin::Saml::Settings.new(:idp_cert_fingerprint => '7292914fc5bffa6f3fe1e43fd47c205395fecfa2')
|
114
|
+
@response = Onelogin::Saml::Response.new(@xmlb64)
|
115
|
+
@response.process(@settings)
|
116
|
+
@response.should_not be_is_valid
|
117
|
+
end
|
118
|
+
|
119
|
+
it "should allow non-ascii characters in attributes" do
|
120
|
+
@xmlb64 = Base64.encode64(File.read(fixture_path("test6-response.xml")))
|
121
|
+
@settings = Onelogin::Saml::Settings.new(:idp_cert_fingerprint => 'afe71c28ef740bc87425be13a2263d37971da1f9')
|
122
|
+
@response = Onelogin::Saml::Response.new(@xmlb64, @settings)
|
123
|
+
@response.should be_is_valid
|
124
|
+
@response.status_code.should == "urn:oasis:names:tc:SAML:2.0:status:Success"
|
125
|
+
@response.saml_attributes['eduPersonAffiliation'].should == 'member'
|
126
|
+
@response.saml_attributes['givenName'].should == 'Canvas'
|
127
|
+
@response.saml_attributes['displayName'].should == 'Canvas Üser'
|
128
|
+
@response.fingerprint_from_idp.should == 'afe71c28ef740bc87425be13a2263d37971da1f9'
|
129
|
+
end
|
130
|
+
|
131
|
+
it "should map OIDs to known attributes" do
|
132
|
+
@xmlb64 = Base64.encode64(File.read(fixture_path("test3-response.xml")))
|
133
|
+
@settings = Onelogin::Saml::Settings.new(:idp_cert_fingerprint => 'afe71c28ef740bc87425be13a2263d37971da1f9')
|
134
|
+
@response = Onelogin::Saml::Response.new(@xmlb64, @settings)
|
135
|
+
@response.should be_is_valid
|
136
|
+
@response.status_code.should == "urn:oasis:names:tc:SAML:2.0:status:Success"
|
137
|
+
@response.saml_attributes['eduPersonAffiliation'].should == 'member'
|
138
|
+
@response.saml_attributes['eduPersonPrincipalName'].should == 'student@example.edu'
|
139
|
+
@response.fingerprint_from_idp.should == 'afe71c28ef740bc87425be13a2263d37971da1f9'
|
140
|
+
end
|
141
|
+
|
142
|
+
it "should not throw an exception when an empty string is passed as the doc" do
|
143
|
+
settings = Onelogin::Saml::Settings.new
|
144
|
+
lambda {
|
145
|
+
r = Onelogin::Saml::Response.new('foo', settings)
|
146
|
+
r.should_not be_is_valid
|
147
|
+
}.should_not raise_error
|
148
|
+
lambda {
|
149
|
+
r = Onelogin::Saml::Response.new('', settings)
|
150
|
+
r.should_not be_is_valid
|
151
|
+
}.should_not raise_error
|
152
|
+
end
|
153
|
+
|
154
|
+
describe "forward_urls" do
|
155
|
+
let(:name_qualifier) { 'foo' }
|
156
|
+
let(:name_id) { 'bar'}
|
157
|
+
let(:session_index) { 'baz' }
|
158
|
+
|
159
|
+
it "should should append the saml request to a url" do
|
160
|
+
settings = Onelogin::Saml::Settings.new(
|
161
|
+
:xmlsec_certificate => fixture_path("test1-cert.pem"),
|
162
|
+
:xmlsec_privatekey => fixture_path("test1-key.pem"),
|
163
|
+
:idp_sso_target_url => "http://example.com/login.php",
|
164
|
+
:idp_slo_target_url => "http://example.com/logout.php"
|
165
|
+
)
|
166
|
+
|
167
|
+
request = Onelogin::Saml::AuthRequest::generate(settings)
|
168
|
+
prefix = "http://example.com/login.php?SAMLRequest="
|
169
|
+
expect(request.forward_url[0...prefix.size]).to eql(prefix)
|
170
|
+
|
171
|
+
request = Onelogin::Saml::LogoutRequest::generate(name_qualifier, name_id, session_index, settings)
|
172
|
+
prefix = "http://example.com/logout.php?SAMLRequest="
|
173
|
+
expect(request.forward_url[0...prefix.size]).to eql(prefix)
|
174
|
+
end
|
175
|
+
|
176
|
+
it "should append the saml request to a url with query parameters" do
|
177
|
+
settings = Onelogin::Saml::Settings.new(
|
178
|
+
:xmlsec_certificate => fixture_path("test1-cert.pem"),
|
179
|
+
:xmlsec_privatekey => fixture_path("test1-key.pem"),
|
180
|
+
:idp_sso_target_url => "http://example.com/login.php?param=foo",
|
181
|
+
:idp_slo_target_url => "http://example.com/logout.php?param=foo"
|
182
|
+
)
|
183
|
+
|
184
|
+
request = Onelogin::Saml::AuthRequest::generate(settings)
|
185
|
+
prefix = "http://example.com/login.php?param=foo&SAMLRequest="
|
186
|
+
expect(request.forward_url[0...prefix.size]).to eql(prefix)
|
187
|
+
|
188
|
+
request = Onelogin::Saml::LogoutRequest::generate(name_qualifier, name_id, session_index, settings)
|
189
|
+
prefix = "http://example.com/logout.php?param=foo&SAMLRequest="
|
190
|
+
expect(request.forward_url[0...prefix.size]).to eql(prefix)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
require 'cgi'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
require File.expand_path(File.dirname(__FILE__) + '/../lib/onelogin/saml.rb')
|
6
|
+
|
7
|
+
Dir[File.expand_path(File.dirname(__FILE__) + '/support/**/*.rb')].each { |f| require f }
|
8
|
+
|
9
|
+
RSpec.configure do |config|
|
10
|
+
FIXTURE_PATH = File.expand_path(File.dirname(__FILE__) + '/fixtures')
|
11
|
+
|
12
|
+
def fixture_path(filename)
|
13
|
+
"#{FIXTURE_PATH}/#{filename}"
|
14
|
+
end
|
15
|
+
|
16
|
+
config.before(:suite) do
|
17
|
+
TestServer.start(ENV['TEST_SERVER_PORT'] || 2345)
|
18
|
+
end
|
19
|
+
|
20
|
+
config.after(:each) do
|
21
|
+
TestServer.reset
|
22
|
+
end
|
23
|
+
|
24
|
+
config.after(:suite) do
|
25
|
+
TestServer.stop
|
26
|
+
end
|
27
|
+
|
28
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
29
|
+
config.run_all_when_everything_filtered = true
|
30
|
+
config.filter_run :focus
|
31
|
+
config.order = 'random'
|
32
|
+
config.color = true
|
33
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'net/http'
|
3
|
+
|
4
|
+
class TestServer
|
5
|
+
class << self
|
6
|
+
def start(port)
|
7
|
+
@port = port
|
8
|
+
@requests = []
|
9
|
+
@process = Process.fork {
|
10
|
+
@server = TCPServer.open(port)
|
11
|
+
work
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def stop
|
16
|
+
Process.kill("TERM", @process)
|
17
|
+
end
|
18
|
+
|
19
|
+
def get(path)
|
20
|
+
Net::HTTP.get_response(URI("http://127.0.0.1:#{@port}#{path}"))
|
21
|
+
end
|
22
|
+
|
23
|
+
def requests
|
24
|
+
get('/requests').body.split("\n")
|
25
|
+
end
|
26
|
+
|
27
|
+
def reset
|
28
|
+
get('/reset')
|
29
|
+
end
|
30
|
+
|
31
|
+
def work
|
32
|
+
loop do
|
33
|
+
socket = @server.accept
|
34
|
+
|
35
|
+
request = socket.gets
|
36
|
+
response = process(request)
|
37
|
+
|
38
|
+
socket.print([
|
39
|
+
"HTTP/1.1 200 OK",
|
40
|
+
"Content-Type: application/xml",
|
41
|
+
"Content-Length: #{response.bytesize}",
|
42
|
+
"Connection: close",
|
43
|
+
"",
|
44
|
+
response
|
45
|
+
].join("\r\n"))
|
46
|
+
|
47
|
+
socket.close
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def process(request)
|
54
|
+
_, path, _ = request.split(' ')
|
55
|
+
|
56
|
+
case path
|
57
|
+
when '/requests'
|
58
|
+
@requests.join("\n")
|
59
|
+
when '/reset'
|
60
|
+
@requests = []
|
61
|
+
'<status>OK</status>'
|
62
|
+
when '/exploit'
|
63
|
+
<<-RESPONSE
|
64
|
+
<!DOCTYPE Response [<!ENTITY file PUBLIC 'p' 'file:///etc/hostname'>]>
|
65
|
+
<Response>&file;</Response>
|
66
|
+
RESPONSE
|
67
|
+
else
|
68
|
+
@requests << request.chomp
|
69
|
+
'<status>RECORDED</status>'
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|