ruby-saml-mod 0.1.30 → 0.2.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/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
|