saml_idp 0.7.2 → 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 +5 -5
- data/Gemfile +1 -1
- data/README.md +71 -55
- data/lib/saml_idp/assertion_builder.rb +28 -3
- data/lib/saml_idp/configurator.rb +9 -3
- data/lib/saml_idp/controller.rb +27 -16
- data/lib/saml_idp/encryptor.rb +0 -1
- data/lib/saml_idp/fingerprint.rb +19 -0
- data/lib/saml_idp/incoming_metadata.rb +31 -1
- data/lib/saml_idp/metadata_builder.rb +25 -9
- data/lib/saml_idp/persisted_metadata.rb +4 -0
- data/lib/saml_idp/request.rb +103 -13
- 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 +16 -6
- data/lib/saml_idp/signable.rb +1 -2
- 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 +20 -15
- data/lib/saml_idp.rb +4 -3
- data/saml_idp.gemspec +46 -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 +45 -7
- data/spec/lib/saml_idp/controller_spec.rb +86 -25
- 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 +134 -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 +153 -64
- data/spec/lib/saml_idp/response_builder_spec.rb +5 -3
- data/spec/lib/saml_idp/saml_response_spec.rb +146 -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 +3 -4
- 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 +107 -5
- data/spec/support/security_helpers.rb +12 -2
- data/spec/xml_security_spec.rb +19 -15
- metadata +146 -80
- data/app/controllers/saml_idp/idp_controller.rb +0 -59
- data/spec/lib/saml_idp/.assertion_builder_spec.rb.swp +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
require 'saml_idp/logout_request_builder'
|
|
2
2
|
|
|
3
3
|
module SamlRequestMacros
|
|
4
|
-
def make_saml_request(requested_saml_acs_url = "https://foo.example.com/saml/consume")
|
|
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')
|
|
@@ -15,23 +15,125 @@ module SamlRequestMacros
|
|
|
15
15
|
'some_name_id',
|
|
16
16
|
OpenSSL::Digest::SHA256
|
|
17
17
|
)
|
|
18
|
-
request_builder.
|
|
18
|
+
Base64.strict_encode64(request_builder.signed)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def
|
|
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
|
+
|
|
31
|
+
def generate_sp_metadata(saml_acs_url = "https://foo.example.com/saml/consume", enable_secure_options = false)
|
|
32
|
+
sp_metadata = OneLogin::RubySaml::Metadata.new
|
|
33
|
+
sp_metadata.generate(saml_settings(saml_acs_url, enable_secure_options), true)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def saml_settings(saml_acs_url = "https://foo.example.com/saml/consume", enable_secure_options = false, security_options: {})
|
|
22
37
|
settings = OneLogin::RubySaml::Settings.new
|
|
23
38
|
settings.assertion_consumer_service_url = saml_acs_url
|
|
24
39
|
settings.issuer = "http://example.com/issuer"
|
|
25
40
|
settings.idp_sso_target_url = "http://idp.com/saml/idp"
|
|
41
|
+
settings.idp_slo_target_url = "http://idp.com/saml/slo"
|
|
42
|
+
settings.assertion_consumer_logout_service_url = 'https://foo.example.com/saml/logout'
|
|
26
43
|
settings.idp_cert_fingerprint = SamlIdp::Default::FINGERPRINT
|
|
27
44
|
settings.name_identifier_format = SamlIdp::Default::NAME_ID_FORMAT
|
|
45
|
+
add_securty_options(settings, default_sp_security_options.merge!(security_options)) if enable_secure_options
|
|
28
46
|
settings
|
|
29
47
|
end
|
|
30
48
|
|
|
49
|
+
def add_securty_options(settings, options = default_sp_security_options)
|
|
50
|
+
# Security section
|
|
51
|
+
settings.idp_cert = SamlIdp::Default::X509_CERTIFICATE
|
|
52
|
+
# Signed embedded singature
|
|
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]
|
|
61
|
+
settings.private_key = sp_pv_key
|
|
62
|
+
settings.certificate = sp_x509_cert
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def idp_configure(saml_acs_url = "https://foo.example.com/saml/consume", enable_secure_options = false)
|
|
66
|
+
SamlIdp.configure do |config|
|
|
67
|
+
config.x509_certificate = SamlIdp::Default::X509_CERTIFICATE
|
|
68
|
+
config.secret_key = SamlIdp::Default::SECRET_KEY
|
|
69
|
+
config.password = nil
|
|
70
|
+
config.algorithm = :sha256
|
|
71
|
+
config.organization_name = 'idp.com'
|
|
72
|
+
config.organization_url = 'http://idp.com'
|
|
73
|
+
config.base_saml_location = 'http://idp.com/saml/idp'
|
|
74
|
+
config.single_logout_service_post_location = 'http://idp.com/saml/idp/logout'
|
|
75
|
+
config.single_logout_service_redirect_location = 'http://idp.com/saml/idp/logout'
|
|
76
|
+
config.attribute_service_location = 'http://idp.com/saml/idp/attribute'
|
|
77
|
+
config.single_service_post_location = 'http://idp.com/saml/idp/sso'
|
|
78
|
+
config.name_id.formats = SamlIdp::Default::NAME_ID_FORMAT
|
|
79
|
+
config.service_provider.metadata_persister = lambda { |_identifier, _service_provider|
|
|
80
|
+
raw_metadata = generate_sp_metadata(saml_acs_url, enable_secure_options)
|
|
81
|
+
SamlIdp::IncomingMetadata.new(raw_metadata).to_h
|
|
82
|
+
}
|
|
83
|
+
config.service_provider.persisted_metadata_getter = lambda { |_identifier, _settings|
|
|
84
|
+
raw_metadata = generate_sp_metadata(saml_acs_url, enable_secure_options)
|
|
85
|
+
SamlIdp::IncomingMetadata.new(raw_metadata).to_h
|
|
86
|
+
}
|
|
87
|
+
config.service_provider.finder = lambda { |_issuer_or_entity_id|
|
|
88
|
+
{
|
|
89
|
+
response_hosts: [URI(saml_acs_url).host],
|
|
90
|
+
acs_url: saml_acs_url,
|
|
91
|
+
cert: sp_x509_cert,
|
|
92
|
+
fingerprint: SamlIdp::Fingerprint.certificate_digest(sp_x509_cert),
|
|
93
|
+
assertion_consumer_logout_service_url: 'https://foo.example.com/saml/logout'
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
end
|
|
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
|
+
|
|
31
110
|
def print_pretty_xml(xml_string)
|
|
32
111
|
doc = REXML::Document.new xml_string
|
|
33
112
|
outbuf = ""
|
|
34
113
|
doc.write(outbuf, 1)
|
|
35
114
|
puts outbuf
|
|
36
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
|
|
37
139
|
end
|
|
@@ -51,11 +51,21 @@ 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
|
|
59
59
|
@signature2 ||= File.read(File.join(File.dirname(__FILE__), 'certificates', 'r1_certificate2_base64'))
|
|
60
60
|
end
|
|
61
|
+
|
|
62
|
+
# Generated by SAML tool https://www.samltool.com/self_signed_certs.php
|
|
63
|
+
def sp_pv_key
|
|
64
|
+
@sp_pv_key ||= File.read(File.join(File.dirname(__FILE__), 'certificates', 'sp_private_key.pem'))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Generated by SAML tool https://www.samltool.com/self_signed_certs.php, expired date is 9999
|
|
68
|
+
def sp_x509_cert
|
|
69
|
+
@sp_x509_cert ||= File.read(File.join(File.dirname(__FILE__), 'certificates', 'sp_x509_cert.crt'))
|
|
70
|
+
end
|
|
61
71
|
end
|
data/spec/xml_security_spec.rb
CHANGED
|
@@ -7,7 +7,7 @@ module SamlIdp
|
|
|
7
7
|
let(:base64cert) { document.elements["//ds:X509Certificate"].text }
|
|
8
8
|
|
|
9
9
|
it "it run validate without throwing NS related exceptions" do
|
|
10
|
-
document.validate_doc(base64cert, true).
|
|
10
|
+
expect(document.validate_doc(base64cert, true)).to be_falsey
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
it "it run validate with throwing NS related exceptions" do
|
|
@@ -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
|
-
document.
|
|
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
|
-
document.
|
|
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
|
-
document.
|
|
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
|
-
document.
|
|
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
|
|
|
@@ -83,7 +87,7 @@ module SamlIdp
|
|
|
83
87
|
document = XMLSecurity::SignedDocument.new(response)
|
|
84
88
|
inclusive_namespaces = document.send(:extract_inclusive_namespaces)
|
|
85
89
|
|
|
86
|
-
inclusive_namespaces.
|
|
90
|
+
expect(inclusive_namespaces).to eq %w[xs]
|
|
87
91
|
end
|
|
88
92
|
|
|
89
93
|
it "support implicit namespace resolution for exclusive canonicalization" do
|
|
@@ -91,7 +95,7 @@ module SamlIdp
|
|
|
91
95
|
document = XMLSecurity::SignedDocument.new(response)
|
|
92
96
|
inclusive_namespaces = document.send(:extract_inclusive_namespaces)
|
|
93
97
|
|
|
94
|
-
inclusive_namespaces.
|
|
98
|
+
expect(inclusive_namespaces).to eq %w[#default saml ds xs xsi]
|
|
95
99
|
end
|
|
96
100
|
|
|
97
101
|
it "return an empty list when inclusive namespace element is missing" do
|
|
@@ -101,7 +105,7 @@ module SamlIdp
|
|
|
101
105
|
document = XMLSecurity::SignedDocument.new(response)
|
|
102
106
|
inclusive_namespaces = document.send(:extract_inclusive_namespaces)
|
|
103
107
|
|
|
104
|
-
inclusive_namespaces.
|
|
108
|
+
expect(inclusive_namespaces).to be_empty
|
|
105
109
|
end
|
|
106
110
|
end
|
|
107
111
|
|
|
@@ -116,20 +120,20 @@ module SamlIdp
|
|
|
116
120
|
|
|
117
121
|
it "be able to validate a good response" do
|
|
118
122
|
Timecop.freeze Time.parse('2012-11-28 17:55:00 UTC') do
|
|
119
|
-
response.
|
|
120
|
-
response.
|
|
123
|
+
allow(response).to receive(:validate_subject_confirmation).and_return(true)
|
|
124
|
+
expect(response).to be_is_valid
|
|
121
125
|
end
|
|
122
126
|
end
|
|
123
127
|
|
|
124
128
|
it "fail before response is valid" do
|
|
125
129
|
Timecop.freeze Time.parse('2012-11-20 17:55:00 UTC') do
|
|
126
|
-
response.
|
|
130
|
+
expect(response).to_not be_is_valid
|
|
127
131
|
end
|
|
128
132
|
end
|
|
129
133
|
|
|
130
134
|
it "fail after response expires" do
|
|
131
135
|
Timecop.freeze Time.parse('2012-11-30 17:55:00 UTC') do
|
|
132
|
-
response.
|
|
136
|
+
expect(response).to_not be_is_valid
|
|
133
137
|
end
|
|
134
138
|
end
|
|
135
139
|
end
|