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.
Files changed (59) hide show
  1. checksums.yaml +5 -5
  2. data/Gemfile +1 -1
  3. data/README.md +71 -55
  4. data/lib/saml_idp/assertion_builder.rb +28 -3
  5. data/lib/saml_idp/configurator.rb +9 -3
  6. data/lib/saml_idp/controller.rb +27 -16
  7. data/lib/saml_idp/encryptor.rb +0 -1
  8. data/lib/saml_idp/fingerprint.rb +19 -0
  9. data/lib/saml_idp/incoming_metadata.rb +31 -1
  10. data/lib/saml_idp/metadata_builder.rb +25 -9
  11. data/lib/saml_idp/persisted_metadata.rb +4 -0
  12. data/lib/saml_idp/request.rb +103 -13
  13. data/lib/saml_idp/response_builder.rb +26 -6
  14. data/lib/saml_idp/saml_response.rb +62 -28
  15. data/lib/saml_idp/service_provider.rb +16 -6
  16. data/lib/saml_idp/signable.rb +1 -2
  17. data/lib/saml_idp/signature_builder.rb +2 -1
  18. data/lib/saml_idp/signed_info_builder.rb +2 -2
  19. data/lib/saml_idp/version.rb +1 -1
  20. data/lib/saml_idp/xml_security.rb +20 -15
  21. data/lib/saml_idp.rb +4 -3
  22. data/saml_idp.gemspec +46 -42
  23. data/spec/acceptance/idp_controller_spec.rb +5 -4
  24. data/spec/lib/saml_idp/algorithmable_spec.rb +6 -6
  25. data/spec/lib/saml_idp/assertion_builder_spec.rb +151 -8
  26. data/spec/lib/saml_idp/attribute_decorator_spec.rb +8 -8
  27. data/spec/lib/saml_idp/configurator_spec.rb +45 -7
  28. data/spec/lib/saml_idp/controller_spec.rb +86 -25
  29. data/spec/lib/saml_idp/encryptor_spec.rb +4 -4
  30. data/spec/lib/saml_idp/fingerprint_spec.rb +14 -0
  31. data/spec/lib/saml_idp/incoming_metadata_spec.rb +134 -0
  32. data/spec/lib/saml_idp/metadata_builder_spec.rb +30 -17
  33. data/spec/lib/saml_idp/name_id_formatter_spec.rb +3 -3
  34. data/spec/lib/saml_idp/request_spec.rb +153 -64
  35. data/spec/lib/saml_idp/response_builder_spec.rb +5 -3
  36. data/spec/lib/saml_idp/saml_response_spec.rb +146 -12
  37. data/spec/lib/saml_idp/service_provider_spec.rb +2 -2
  38. data/spec/lib/saml_idp/signable_spec.rb +1 -1
  39. data/spec/lib/saml_idp/signature_builder_spec.rb +2 -2
  40. data/spec/lib/saml_idp/signed_info_builder_spec.rb +3 -3
  41. data/spec/rails_app/app/controllers/saml_controller.rb +1 -1
  42. data/spec/rails_app/app/controllers/saml_idp_controller.rb +55 -3
  43. data/{app → spec/rails_app/app}/views/saml_idp/idp/new.html.erb +3 -4
  44. data/{app → spec/rails_app/app}/views/saml_idp/idp/saml_post.html.erb +1 -1
  45. data/spec/rails_app/config/application.rb +1 -6
  46. data/spec/rails_app/config/boot.rb +1 -1
  47. data/spec/rails_app/config/environments/development.rb +2 -5
  48. data/spec/rails_app/config/environments/production.rb +1 -0
  49. data/spec/rails_app/config/environments/test.rb +1 -0
  50. data/spec/spec_helper.rb +23 -1
  51. data/spec/support/certificates/sp_cert_req.csr +12 -0
  52. data/spec/support/certificates/sp_private_key.pem +16 -0
  53. data/spec/support/certificates/sp_x509_cert.crt +18 -0
  54. data/spec/support/saml_request_macros.rb +107 -5
  55. data/spec/support/security_helpers.rb +12 -2
  56. data/spec/xml_security_spec.rb +19 -15
  57. metadata +146 -80
  58. data/app/controllers/saml_idp/idp_controller.rb +0 -59
  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.create(saml_settings(requested_saml_acs_url))
7
- CGI.unescape(auth_url.split("=").last)
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.encoded
18
+ Base64.strict_encode64(request_builder.signed)
19
19
  end
20
20
 
21
- def saml_settings(saml_acs_url = "https://foo.example.com/saml/consume")
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 signature_1
55
- @signature1 ||= File.read(File.join(File.dirname(__FILE__), 'certificates', 'certificate1'))
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
@@ -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).should be_falsey
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 element missing in response (ds:X509Certificate)"
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.validate("F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72").should be_truthy
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.validate("28:74:9B:E8:1F:E8:10:9C:A8:7C:A9:C3:E3:C5:01:6C:92:1C:B4:BA").should be_truthy
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.validate("F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72").should be_truthy
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.validate("F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72").should be_truthy
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.should == %w[xs]
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.should == %w[#default saml ds xs xsi]
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.should be_empty
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.stub(:validate_subject_confirmation).and_return(true)
120
- response.should be_is_valid
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.should_not be_is_valid
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.should_not be_is_valid
136
+ expect(response).to_not be_is_valid
133
137
  end
134
138
  end
135
139
  end