saml_idp 0.8.0 → 0.15.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +49 -47
  3. data/lib/saml_idp/assertion_builder.rb +28 -3
  4. data/lib/saml_idp/configurator.rb +6 -1
  5. data/lib/saml_idp/controller.rb +19 -11
  6. data/lib/saml_idp/encryptor.rb +0 -1
  7. data/lib/saml_idp/fingerprint.rb +19 -0
  8. data/lib/saml_idp/incoming_metadata.rb +18 -0
  9. data/lib/saml_idp/metadata_builder.rb +23 -8
  10. data/lib/saml_idp/persisted_metadata.rb +4 -0
  11. data/lib/saml_idp/request.rb +13 -6
  12. data/lib/saml_idp/response_builder.rb +26 -6
  13. data/lib/saml_idp/saml_response.rb +62 -28
  14. data/lib/saml_idp/service_provider.rb +1 -6
  15. data/lib/saml_idp/signable.rb +1 -2
  16. data/lib/saml_idp/version.rb +1 -1
  17. data/lib/saml_idp/xml_security.rb +1 -1
  18. data/lib/saml_idp.rb +2 -1
  19. data/saml_idp.gemspec +31 -31
  20. data/spec/lib/saml_idp/assertion_builder_spec.rb +143 -0
  21. data/spec/lib/saml_idp/configurator_spec.rb +2 -0
  22. data/spec/lib/saml_idp/controller_spec.rb +24 -0
  23. data/spec/lib/saml_idp/fingerprint_spec.rb +14 -0
  24. data/spec/lib/saml_idp/incoming_metadata_spec.rb +20 -1
  25. data/spec/lib/saml_idp/metadata_builder_spec.rb +23 -0
  26. data/spec/lib/saml_idp/request_spec.rb +43 -9
  27. data/spec/lib/saml_idp/response_builder_spec.rb +3 -1
  28. data/spec/lib/saml_idp/saml_response_spec.rb +122 -7
  29. data/spec/rails_app/app/controllers/saml_controller.rb +1 -5
  30. data/spec/rails_app/app/controllers/saml_idp_controller.rb +55 -3
  31. data/{app → spec/rails_app/app}/views/saml_idp/idp/new.html.erb +1 -5
  32. data/{app → spec/rails_app/app}/views/saml_idp/idp/saml_post.html.erb +1 -1
  33. data/spec/rails_app/config/application.rb +1 -0
  34. data/spec/rails_app/config/boot.rb +1 -1
  35. data/spec/rails_app/config/environments/development.rb +2 -0
  36. data/spec/spec_helper.rb +20 -1
  37. data/spec/support/certificates/sp_cert_req.csr +12 -0
  38. data/spec/support/certificates/sp_private_key.pem +16 -0
  39. data/spec/support/certificates/sp_x509_cert.crt +18 -0
  40. data/spec/support/saml_request_macros.rb +62 -3
  41. data/spec/support/security_helpers.rb +10 -0
  42. metadata +83 -61
  43. data/app/controllers/saml_idp/idp_controller.rb +0 -59
@@ -1,8 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'saml_idp/assertion_builder'
2
4
  require 'saml_idp/response_builder'
3
5
  module SamlIdp
4
6
  class SamlResponse
5
- attr_accessor :assertion_with_signature
6
7
  attr_accessor :reference_id
7
8
  attr_accessor :response_id
8
9
  attr_accessor :issuer_uri
@@ -17,20 +18,32 @@ module SamlIdp
17
18
  attr_accessor :expiry
18
19
  attr_accessor :encryption_opts
19
20
  attr_accessor :session_expiry
21
+ attr_accessor :name_id_formats_opts
22
+ attr_accessor :asserted_attributes_opts
23
+ attr_accessor :signed_message_opts
24
+ attr_accessor :signed_assertion_opts
25
+ attr_accessor :compression_opts
26
+
27
+ def initialize(
28
+ reference_id,
29
+ response_id,
30
+ issuer_uri,
31
+ principal,
32
+ audience_uri,
33
+ saml_request_id,
34
+ saml_acs_url,
35
+ algorithm,
36
+ authn_context_classref,
37
+ expiry = 60 * 60,
38
+ encryption_opts = nil,
39
+ session_expiry = 0,
40
+ name_id_formats_opts = nil,
41
+ asserted_attributes_opts = nil,
42
+ signed_message_opts = false,
43
+ signed_assertion_opts = true,
44
+ compression_opts = false
45
+ )
20
46
 
21
- def initialize(reference_id,
22
- response_id,
23
- issuer_uri,
24
- principal,
25
- audience_uri,
26
- saml_request_id,
27
- saml_acs_url,
28
- algorithm,
29
- authn_context_classref,
30
- expiry=60*60,
31
- encryption_opts=nil,
32
- session_expiry=0
33
- )
34
47
  self.reference_id = reference_id
35
48
  self.response_id = response_id
36
49
  self.issuer_uri = issuer_uri
@@ -45,38 +58,59 @@ module SamlIdp
45
58
  self.expiry = expiry
46
59
  self.encryption_opts = encryption_opts
47
60
  self.session_expiry = session_expiry
61
+ self.signed_message_opts = signed_message_opts
62
+ self.name_id_formats_opts = name_id_formats_opts
63
+ self.asserted_attributes_opts = asserted_attributes_opts
64
+ self.signed_assertion_opts = signed_assertion_opts
65
+ self.name_id_formats_opts = name_id_formats_opts
66
+ self.asserted_attributes_opts = asserted_attributes_opts
67
+ self.compression_opts = compression_opts
48
68
  end
49
69
 
50
70
  def build
51
- @built ||= response_builder.encoded
71
+ @build ||= encoded_message
52
72
  end
53
73
 
54
74
  def signed_assertion
55
75
  if encryption_opts
56
76
  assertion_builder.encrypt(sign: true)
57
- else
77
+ elsif signed_assertion_opts
58
78
  assertion_builder.signed
79
+ else
80
+ assertion_builder.raw
59
81
  end
60
82
  end
61
83
  private :signed_assertion
62
84
 
85
+ def encoded_message
86
+ if signed_message_opts
87
+ response_builder.encoded(signed_message: true, compress: compression_opts)
88
+ else
89
+ response_builder.encoded(signed_message: false, compress: compression_opts)
90
+ end
91
+ end
92
+ private :encoded_message
93
+
63
94
  def response_builder
64
- ResponseBuilder.new(response_id, issuer_uri, saml_acs_url, saml_request_id, signed_assertion)
95
+ ResponseBuilder.new(response_id, issuer_uri, saml_acs_url, saml_request_id, signed_assertion, algorithm)
65
96
  end
66
97
  private :response_builder
67
98
 
68
99
  def assertion_builder
69
- @assertion_builder ||= AssertionBuilder.new reference_id,
70
- issuer_uri,
71
- principal,
72
- audience_uri,
73
- saml_request_id,
74
- saml_acs_url,
75
- algorithm,
76
- authn_context_classref,
77
- expiry,
78
- encryption_opts,
79
- session_expiry
100
+ @assertion_builder ||=
101
+ AssertionBuilder.new SecureRandom.uuid,
102
+ issuer_uri,
103
+ principal,
104
+ audience_uri,
105
+ saml_request_id,
106
+ saml_acs_url,
107
+ algorithm,
108
+ authn_context_classref,
109
+ expiry,
110
+ encryption_opts,
111
+ session_expiry,
112
+ name_id_formats_opts,
113
+ asserted_attributes_opts
80
114
  end
81
115
  private :assertion_builder
82
116
  end
@@ -22,18 +22,13 @@ module SamlIdp
22
22
  end
23
23
 
24
24
  def valid_signature?(doc, require_signature = false)
25
- if require_signature || should_validate_signature?
25
+ if require_signature || attributes[:validate_signature]
26
26
  doc.valid_signature?(fingerprint)
27
27
  else
28
28
  true
29
29
  end
30
30
  end
31
31
 
32
- def should_validate_signature?
33
- attributes[:validate_signature] ||
34
- current_metadata.respond_to?(:sign_assertions?) && current_metadata.sign_assertions?
35
- end
36
-
37
32
  def refresh_metadata
38
33
  fresh = fresh_incoming_metadata
39
34
  if valid_signature?(fresh.document)
@@ -108,8 +108,7 @@ module SamlIdp
108
108
  canon_algorithm = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
109
109
  canon_hashed_element = noko_raw.canonicalize(canon_algorithm, inclusive_namespaces)
110
110
  digest_algorithm = get_algorithm
111
-
112
- hash = digest_algorithm.digest(canon_hashed_element)
111
+ hash = digest_algorithm.digest(canon_hashed_element)
113
112
  Base64.strict_encode64(hash).gsub(/\n/, '')
114
113
  end
115
114
  private :digest
@@ -1,4 +1,4 @@
1
1
  # encoding: utf-8
2
2
  module SamlIdp
3
- VERSION = '0.8.0'
3
+ VERSION = '0.15.0'
4
4
  end
@@ -108,7 +108,7 @@ module SamlIdp
108
108
  canon_algorithm = canon_algorithm REXML::XPath.first(ref, '//ds:CanonicalizationMethod', 'ds' => DSIG)
109
109
  canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
110
110
 
111
- digest_algorithm = algorithm(REXML::XPath.first(ref, "//ds:DigestMethod"))
111
+ digest_algorithm = algorithm(REXML::XPath.first(ref, "//ds:DigestMethod", {'ds' => DSIG}))
112
112
 
113
113
  hash = digest_algorithm.digest(canon_hashed_element)
114
114
  digest_value = Base64.decode64(REXML::XPath.first(ref, "//ds:DigestValue", {"ds"=>DSIG}).text)
data/lib/saml_idp.rb CHANGED
@@ -8,7 +8,8 @@ module SamlIdp
8
8
  require 'saml_idp/default'
9
9
  require 'saml_idp/metadata_builder'
10
10
  require 'saml_idp/version'
11
- require 'saml_idp/engine' if defined?(::Rails) && Rails::VERSION::MAJOR > 2
11
+ require 'saml_idp/fingerprint'
12
+ require 'saml_idp/engine' if defined?(::Rails)
12
13
 
13
14
  def self.config
14
15
  @config ||= SamlIdp::Configurator.new
data/saml_idp.gemspec CHANGED
@@ -1,62 +1,62 @@
1
1
  # -*- encoding: utf-8 -*-
2
- $:.push File.expand_path("../lib", __FILE__)
3
- require "saml_idp/version"
2
+
3
+ $LOAD_PATH.push File.expand_path('lib', __dir__)
4
+ require 'saml_idp/version'
4
5
 
5
6
  Gem::Specification.new do |s|
6
7
  s.name = %q{saml_idp}
7
8
  s.version = SamlIdp::VERSION
8
9
  s.platform = Gem::Platform::RUBY
9
- s.authors = ["Jon Phenow"]
10
+ s.authors = ['Jon Phenow']
10
11
  s.email = 'jon.phenow@sportngin.com'
11
12
  s.homepage = 'https://github.com/saml-idp/saml_idp'
12
13
  s.summary = 'SAML Indentity Provider for Ruby'
13
14
  s.description = 'SAML IdP (Identity Provider) Library for Ruby'
14
- s.date = Time.now.utc.strftime("%Y-%m-%d")
15
- s.files = Dir['app/**/*', 'lib/**/*', 'LICENSE', 'README.md', 'Gemfile', 'saml_idp.gemspec']
16
- s.required_ruby_version = '>= 2.2'
15
+ s.date = Time.now.utc.strftime('%Y-%m-%d')
16
+ s.files = Dir['lib/**/*', 'LICENSE', 'README.md', 'Gemfile', 'saml_idp.gemspec']
17
+ s.required_ruby_version = '>= 2.5'
17
18
  s.license = 'MIT'
18
19
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
20
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
- s.require_paths = ["lib"]
21
+ s.require_paths = ['lib']
21
22
  s.rdoc_options = ['--charset=UTF-8']
22
23
  s.metadata = {
23
- 'homepage_uri' => 'https://github.com/saml-idp/saml_idp',
24
- 'source_code_uri' => 'https://github.com/saml-idp/saml_idp',
25
- 'bug_tracker_uri' => 'https://github.com/saml-idp/saml_idp/issues',
24
+ 'homepage_uri' => 'https://github.com/saml-idp/saml_idp',
25
+ 'source_code_uri' => 'https://github.com/saml-idp/saml_idp',
26
+ 'bug_tracker_uri' => 'https://github.com/saml-idp/saml_idp/issues',
26
27
  'documentation_uri' => "http://rdoc.info/gems/saml_idp/#{SamlIdp::VERSION}"
27
28
  }
28
29
 
29
30
  s.post_install_message = <<-INST
30
- If you're just recently updating saml_idp - please be aware we've changed the default
31
- certificate. See the PR and a description of why we've done this here:
32
- https://github.com/saml-idp/saml_idp/pull/29
33
-
34
- If you just need to see the certificate `bundle open saml_idp` and go to
35
- `lib/saml_idp/default.rb`
31
+ If you're just recently updating saml_idp - please be aware we've changed the default
32
+ certificate. See the PR and a description of why we've done this here:
33
+ https://github.com/saml-idp/saml_idp/pull/29
36
34
 
37
- Similarly, please see the README about certificates - you should avoid using the
38
- defaults in a Production environment. Post any issues you to github.
35
+ If you just need to see the certificate `bundle open saml_idp` and go to
36
+ `lib/saml_idp/default.rb`
39
37
 
40
- ** New in Version 0.3.0 **
38
+ Similarly, please see the README about certificates - you should avoid using the
39
+ defaults in a Production environment. Post any issues you to github.
41
40
 
42
- Encrypted Assertions require the xmlenc gem. See the example in the Controller
43
- section of the README.
41
+ ** New in Version 0.3.0 **
42
+ Encrypted Assertions require the xmlenc gem. See the example in the Controller
43
+ section of the README.
44
44
  INST
45
45
 
46
- s.add_dependency('activesupport', '>= 3.2')
47
- s.add_dependency('uuid', '>= 2.3')
46
+ s.add_dependency('activesupport', '>= 5.2')
48
47
  s.add_dependency('builder', '>= 3.0')
49
48
  s.add_dependency('nokogiri', '>= 1.6.2')
49
+ s.add_dependency('rexml')
50
+ s.add_dependency('xmlenc', '>= 0.7.1')
50
51
 
52
+ s.add_development_dependency('activeresource', '>= 5.1')
53
+ s.add_development_dependency('appraisal')
54
+ s.add_development_dependency('byebug')
55
+ s.add_development_dependency('capybara', '>= 2.16')
56
+ s.add_development_dependency('rails', '>= 5.2')
51
57
  s.add_development_dependency('rake')
52
- s.add_development_dependency('simplecov')
53
58
  s.add_development_dependency('rspec', '>= 3.7.0')
54
- s.add_development_dependency('ruby-saml', '>= 1.5')
55
- s.add_development_dependency('rails', '>= 3.2')
56
- s.add_development_dependency('activeresource', '>= 3.2')
57
- s.add_development_dependency('capybara', '>= 2.16')
59
+ s.add_development_dependency('ruby-saml', '>= 1.7.2')
60
+ s.add_development_dependency('simplecov')
58
61
  s.add_development_dependency('timecop', '>= 0.8')
59
- s.add_development_dependency('xmlenc', '>= 0.6.4')
60
- s.add_development_dependency('appraisal')
61
62
  end
62
-
@@ -19,6 +19,9 @@ module SamlIdp
19
19
  key_transport: 'rsa-oaep-mgf1p',
20
20
  }
21
21
  end
22
+ let(:session_expiry) { nil }
23
+ let(:name_id_formats_opt) { nil }
24
+ let(:asserted_attributes_opt) { nil }
22
25
  subject { described_class.new(
23
26
  reference_id,
24
27
  issuer_uri,
@@ -103,6 +106,76 @@ module SamlIdp
103
106
  expect(encrypted_xml).to_not match(audience_uri)
104
107
  end
105
108
 
109
+ describe "with name_id_formats_opt" do
110
+ let(:name_id_formats_opt) {
111
+ {
112
+ persistent: -> (principal) {
113
+ principal.unique_identifier
114
+ }
115
+ }
116
+ }
117
+ it "delegates name_id_formats to opts" do
118
+ UserWithUniqueId = Struct.new(:unique_identifier, :email, :asserted_attributes)
119
+ principal = UserWithUniqueId.new('unique_identifier_123456', 'foo@example.com', { emailAddress: { getter: :email } })
120
+ builder = described_class.new(
121
+ reference_id,
122
+ issuer_uri,
123
+ principal,
124
+ audience_uri,
125
+ saml_request_id,
126
+ saml_acs_url,
127
+ algorithm,
128
+ authn_context_classref,
129
+ expiry,
130
+ encryption_opts,
131
+ session_expiry,
132
+ name_id_formats_opt,
133
+ asserted_attributes_opt
134
+ )
135
+ Timecop.travel(Time.zone.local(2010, 6, 1, 13, 0, 0)) do
136
+ expect(builder.raw).to eq("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"_abc\" IssueInstant=\"2010-06-01T13:00:00Z\" Version=\"2.0\"><Issuer>http://sportngin.com</Issuer><Subject><NameID Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent\">unique_identifier_123456</NameID><SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><SubjectConfirmationData InResponseTo=\"123\" NotOnOrAfter=\"2010-06-01T13:03:00Z\" Recipient=\"http://saml.acs.url\"></SubjectConfirmationData></SubjectConfirmation></Subject><Conditions NotBefore=\"2010-06-01T12:59:55Z\" NotOnOrAfter=\"2010-06-01T16:00:00Z\"><AudienceRestriction><Audience>http://example.com</Audience></AudienceRestriction></Conditions><AuthnStatement AuthnInstant=\"2010-06-01T13:00:00Z\" SessionIndex=\"_abc\"><AuthnContext><AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef></AuthnContext></AuthnStatement><AttributeStatement><Attribute Name=\"emailAddress\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:uri\" FriendlyName=\"emailAddress\"><AttributeValue>foo@example.com</AttributeValue></Attribute></AttributeStatement></Assertion>")
137
+ end
138
+ end
139
+ end
140
+
141
+ describe "with asserted_attributes_opt" do
142
+ let(:asserted_attributes_opt) {
143
+ {
144
+ 'GivenName' => {
145
+ getter: :first_name
146
+ },
147
+ 'SurName' => {
148
+ getter: -> (principal) {
149
+ principal.last_name
150
+ }
151
+ }
152
+ }
153
+ }
154
+
155
+ it "delegates asserted_attributes to opts" do
156
+ UserWithName = Struct.new(:email, :first_name, :last_name)
157
+ principal = UserWithName.new('foo@example.com', 'George', 'Washington')
158
+ builder = described_class.new(
159
+ reference_id,
160
+ issuer_uri,
161
+ principal,
162
+ audience_uri,
163
+ saml_request_id,
164
+ saml_acs_url,
165
+ algorithm,
166
+ authn_context_classref,
167
+ expiry,
168
+ encryption_opts,
169
+ session_expiry,
170
+ name_id_formats_opt,
171
+ asserted_attributes_opt
172
+ )
173
+ Timecop.travel(Time.zone.local(2010, 6, 1, 13, 0, 0)) do
174
+ expect(builder.raw).to eq("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"_abc\" IssueInstant=\"2010-06-01T13:00:00Z\" Version=\"2.0\"><Issuer>http://sportngin.com</Issuer><Subject><NameID Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\">foo@example.com</NameID><SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><SubjectConfirmationData InResponseTo=\"123\" NotOnOrAfter=\"2010-06-01T13:03:00Z\" Recipient=\"http://saml.acs.url\"></SubjectConfirmationData></SubjectConfirmation></Subject><Conditions NotBefore=\"2010-06-01T12:59:55Z\" NotOnOrAfter=\"2010-06-01T16:00:00Z\"><AudienceRestriction><Audience>http://example.com</Audience></AudienceRestriction></Conditions><AuthnStatement AuthnInstant=\"2010-06-01T13:00:00Z\" SessionIndex=\"_abc\"><AuthnContext><AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef></AuthnContext></AuthnStatement><AttributeStatement><Attribute Name=\"GivenName\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:uri\" FriendlyName=\"GivenName\"><AttributeValue>George</AttributeValue></Attribute><Attribute Name=\"SurName\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:uri\" FriendlyName=\"SurName\"><AttributeValue>Washington</AttributeValue></Attribute></AttributeStatement></Assertion>")
175
+ end
176
+ end
177
+ end
178
+
106
179
  describe "with custom session_expiry configuration" do
107
180
  let(:config) { SamlIdp::Configurator.new }
108
181
  before do
@@ -126,5 +199,75 @@ module SamlIdp
126
199
  expect(builder.session_expiry).to eq(8)
127
200
  end
128
201
  end
202
+
203
+ describe "with name_id_formats_opt" do
204
+ let(:name_id_formats_opt) {
205
+ {
206
+ persistent: -> (principal) {
207
+ principal.unique_identifier
208
+ }
209
+ }
210
+ }
211
+ it "delegates name_id_formats to opts" do
212
+ UserWithUniqueId = Struct.new(:unique_identifier, :email, :asserted_attributes)
213
+ principal = UserWithUniqueId.new('unique_identifier_123456', 'foo@example.com', { emailAddress: { getter: :email } })
214
+ builder = described_class.new(
215
+ reference_id,
216
+ issuer_uri,
217
+ principal,
218
+ audience_uri,
219
+ saml_request_id,
220
+ saml_acs_url,
221
+ algorithm,
222
+ authn_context_classref,
223
+ expiry,
224
+ encryption_opts,
225
+ session_expiry,
226
+ name_id_formats_opt,
227
+ asserted_attributes_opt
228
+ )
229
+ Timecop.travel(Time.zone.local(2010, 6, 1, 13, 0, 0)) do
230
+ expect(builder.raw).to eq("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"_abc\" IssueInstant=\"2010-06-01T13:00:00Z\" Version=\"2.0\"><Issuer>http://sportngin.com</Issuer><Subject><NameID Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:persistent\">unique_identifier_123456</NameID><SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><SubjectConfirmationData InResponseTo=\"123\" NotOnOrAfter=\"2010-06-01T13:03:00Z\" Recipient=\"http://saml.acs.url\"></SubjectConfirmationData></SubjectConfirmation></Subject><Conditions NotBefore=\"2010-06-01T12:59:55Z\" NotOnOrAfter=\"2010-06-01T16:00:00Z\"><AudienceRestriction><Audience>http://example.com</Audience></AudienceRestriction></Conditions><AuthnStatement AuthnInstant=\"2010-06-01T13:00:00Z\" SessionIndex=\"_abc\"><AuthnContext><AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef></AuthnContext></AuthnStatement><AttributeStatement><Attribute Name=\"emailAddress\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:uri\" FriendlyName=\"emailAddress\"><AttributeValue>foo@example.com</AttributeValue></Attribute></AttributeStatement></Assertion>")
231
+ end
232
+ end
233
+ end
234
+
235
+ describe "with asserted_attributes_opt" do
236
+ let(:asserted_attributes_opt) {
237
+ {
238
+ 'GivenName' => {
239
+ getter: :first_name
240
+ },
241
+ 'SurName' => {
242
+ getter: -> (principal) {
243
+ principal.last_name
244
+ }
245
+ }
246
+ }
247
+ }
248
+
249
+ it "delegates asserted_attributes to opts" do
250
+ UserWithName = Struct.new(:email, :first_name, :last_name)
251
+ principal = UserWithName.new('foo@example.com', 'George', 'Washington')
252
+ builder = described_class.new(
253
+ reference_id,
254
+ issuer_uri,
255
+ principal,
256
+ audience_uri,
257
+ saml_request_id,
258
+ saml_acs_url,
259
+ algorithm,
260
+ authn_context_classref,
261
+ expiry,
262
+ encryption_opts,
263
+ session_expiry,
264
+ name_id_formats_opt,
265
+ asserted_attributes_opt
266
+ )
267
+ Timecop.travel(Time.zone.local(2010, 6, 1, 13, 0, 0)) do
268
+ expect(builder.raw).to eq("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"_abc\" IssueInstant=\"2010-06-01T13:00:00Z\" Version=\"2.0\"><Issuer>http://sportngin.com</Issuer><Subject><NameID Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\">foo@example.com</NameID><SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><SubjectConfirmationData InResponseTo=\"123\" NotOnOrAfter=\"2010-06-01T13:03:00Z\" Recipient=\"http://saml.acs.url\"></SubjectConfirmationData></SubjectConfirmation></Subject><Conditions NotBefore=\"2010-06-01T12:59:55Z\" NotOnOrAfter=\"2010-06-01T16:00:00Z\"><AudienceRestriction><Audience>http://example.com</Audience></AudienceRestriction></Conditions><AuthnStatement AuthnInstant=\"2010-06-01T13:00:00Z\" SessionIndex=\"_abc\"><AuthnContext><AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef></AuthnContext></AuthnStatement><AttributeStatement><Attribute Name=\"GivenName\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:uri\" FriendlyName=\"GivenName\"><AttributeValue>George</AttributeValue></Attribute><Attribute Name=\"SurName\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:uri\" FriendlyName=\"SurName\"><AttributeValue>Washington</AttributeValue></Attribute></AttributeStatement></Assertion>")
269
+ end
270
+ end
271
+ end
129
272
  end
130
273
  end
@@ -9,6 +9,7 @@ module SamlIdp
9
9
  it { should respond_to :base_saml_location }
10
10
  it { should respond_to :reference_id_generator }
11
11
  it { should respond_to :attribute_service_location }
12
+ it { should respond_to :single_service_redirect_location }
12
13
  it { should respond_to :single_service_post_location }
13
14
  it { should respond_to :single_logout_service_post_location }
14
15
  it { should respond_to :single_logout_service_redirect_location }
@@ -16,6 +17,7 @@ module SamlIdp
16
17
  it { should respond_to :attributes }
17
18
  it { should respond_to :service_provider }
18
19
  it { should respond_to :session_expiry }
20
+ it { should respond_to :logger }
19
21
 
20
22
  it "has a valid x509_certificate" do
21
23
  expect(subject.x509_certificate).to eq(Default::X509_CERTIFICATE)
@@ -21,6 +21,30 @@ describe SamlIdp::Controller do
21
21
  expect(saml_acs_url).to eq(requested_saml_acs_url)
22
22
  end
23
23
 
24
+ context "When SP metadata required to validate auth request signature" do
25
+ before do
26
+ idp_configure("https://foo.example.com/saml/consume", true)
27
+ params[:SAMLRequest] = make_saml_request("https://foo.example.com/saml/consume", true)
28
+ end
29
+
30
+ it 'SP metadata sign_authn_request attribute should be true' do
31
+ # Signed auth request will be true in the metadata
32
+ expect(SamlIdp.config.service_provider.persisted_metadata_getter.call(nil,nil)[:sign_authn_request]).to eq(true)
33
+ end
34
+
35
+ it 'should call xml signature validation method' do
36
+ signed_doc = SamlIdp::XMLSecurity::SignedDocument.new(params[:SAMLRequest])
37
+ allow(signed_doc).to receive(:validate).and_return(true)
38
+ allow(SamlIdp::XMLSecurity::SignedDocument).to receive(:new).and_return(signed_doc)
39
+ validate_saml_request
40
+ expect(signed_doc).to have_received(:validate).once
41
+ end
42
+
43
+ it 'should successfully validate signature' do
44
+ expect(validate_saml_request).to eq(true)
45
+ end
46
+ end
47
+
24
48
  context "SAML Responses" do
25
49
  let(:principal) { double email_address: "foo@example.com" }
26
50
  let (:encryption_opts) do
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ module SamlIdp
4
+ describe Fingerprint do
5
+ describe "certificate_digest" do
6
+ let(:cert) { sp_x509_cert }
7
+ let(:fingerprint) { "a2:cb:f6:6b:bc:2a:33:b9:4f:f3:c3:7e:26:a4:21:cd:41:83:ef:26:88:fa:ba:71:37:40:07:3e:d5:76:04:b7" }
8
+
9
+ it "returns the fingerprint string" do
10
+ expect(Fingerprint.certificate_digest(cert, :sha256)).to eq(fingerprint)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -3,7 +3,7 @@ module SamlIdp
3
3
 
4
4
  metadata_1 = <<-eos
5
5
  <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="test" entityID="https://test-saml.com/saml">
6
- <md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol" AuthnRequestsSigned="true" WantAssertionsSigned="false">
6
+ <md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol" AuthnRequestsSigned="false" WantAssertionsSigned="false">
7
7
  </md:SPSSODescriptor>
8
8
  </md:EntityDescriptor>
9
9
  eos
@@ -22,20 +22,39 @@ module SamlIdp
22
22
  </md:EntityDescriptor>
23
23
  eos
24
24
 
25
+ metadata_4 = <<-eos
26
+ <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="test" entityID="https://test-saml.com/saml">
27
+ <md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
28
+ </md:SPSSODescriptor>
29
+ </md:EntityDescriptor>
30
+ eos
31
+
25
32
  describe IncomingMetadata do
26
33
  it 'should properly set sign_assertions to false' do
27
34
  metadata = SamlIdp::IncomingMetadata.new(metadata_1)
28
35
  expect(metadata.sign_assertions).to eq(false)
36
+ expect(metadata.sign_authn_request).to eq(false)
37
+ end
38
+
39
+ it 'should properly set entity_id as https://test-saml.com/saml' do
40
+ metadata = SamlIdp::IncomingMetadata.new(metadata_1)
41
+ expect(metadata.entity_id).to eq('https://test-saml.com/saml')
29
42
  end
30
43
 
31
44
  it 'should properly set sign_assertions to true' do
32
45
  metadata = SamlIdp::IncomingMetadata.new(metadata_2)
33
46
  expect(metadata.sign_assertions).to eq(true)
47
+ expect(metadata.sign_authn_request).to eq(true)
34
48
  end
35
49
 
36
50
  it 'should properly set sign_assertions to false when WantAssertionsSigned is not included' do
37
51
  metadata = SamlIdp::IncomingMetadata.new(metadata_3)
38
52
  expect(metadata.sign_assertions).to eq(false)
39
53
  end
54
+
55
+ it 'should properly set sign_authn_request to false when AuthnRequestsSigned is not included' do
56
+ metadata = SamlIdp::IncomingMetadata.new(metadata_4)
57
+ expect(metadata.sign_authn_request).to eq(false)
58
+ end
40
59
  end
41
60
  end
@@ -11,7 +11,30 @@ module SamlIdp
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.configurator.single_logout_service_redirect_location = 'https://example.com/saml/logout'
14
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"')
15
38
  end
16
39
 
17
40
  context "technical contact" do