saml2 2.0.2 → 2.1.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/lib/saml2.rb +2 -0
  3. data/lib/saml2/assertion.rb +6 -0
  4. data/lib/saml2/attribute.rb +45 -13
  5. data/lib/saml2/attribute/x500.rb +32 -19
  6. data/lib/saml2/attribute_consuming_service.rb +52 -4
  7. data/lib/saml2/authn_request.rb +39 -3
  8. data/lib/saml2/authn_statement.rb +23 -11
  9. data/lib/saml2/base.rb +36 -0
  10. data/lib/saml2/bindings.rb +3 -1
  11. data/lib/saml2/bindings/http_post.rb +17 -1
  12. data/lib/saml2/bindings/http_redirect.rb +54 -9
  13. data/lib/saml2/conditions.rb +43 -16
  14. data/lib/saml2/contact.rb +17 -6
  15. data/lib/saml2/endpoint.rb +13 -0
  16. data/lib/saml2/engine.rb +2 -0
  17. data/lib/saml2/entity.rb +20 -0
  18. data/lib/saml2/identity_provider.rb +11 -1
  19. data/lib/saml2/indexed_object.rb +13 -3
  20. data/lib/saml2/key.rb +89 -32
  21. data/lib/saml2/localized_name.rb +8 -0
  22. data/lib/saml2/logout_request.rb +12 -3
  23. data/lib/saml2/logout_response.rb +9 -0
  24. data/lib/saml2/message.rb +38 -7
  25. data/lib/saml2/name_id.rb +42 -16
  26. data/lib/saml2/namespaces.rb +10 -8
  27. data/lib/saml2/organization.rb +5 -0
  28. data/lib/saml2/organization_and_contacts.rb +5 -0
  29. data/lib/saml2/request.rb +3 -0
  30. data/lib/saml2/requested_authn_context.rb +7 -1
  31. data/lib/saml2/response.rb +20 -2
  32. data/lib/saml2/role.rb +12 -2
  33. data/lib/saml2/schemas.rb +2 -0
  34. data/lib/saml2/service_provider.rb +6 -0
  35. data/lib/saml2/signable.rb +32 -2
  36. data/lib/saml2/sso.rb +7 -0
  37. data/lib/saml2/status.rb +8 -1
  38. data/lib/saml2/status_response.rb +7 -1
  39. data/lib/saml2/subject.rb +22 -5
  40. data/lib/saml2/version.rb +3 -1
  41. data/spec/lib/bindings/http_redirect_spec.rb +23 -2
  42. data/spec/lib/conditions_spec.rb +10 -11
  43. data/spec/lib/identity_provider_spec.rb +1 -1
  44. data/spec/lib/service_provider_spec.rb +7 -2
  45. metadata +5 -5
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SAML2
2
4
  module Schemas
3
5
  def self.federation
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'nokogiri'
2
4
 
3
5
  require 'saml2/endpoint'
@@ -11,12 +13,14 @@ module SAML2
11
13
  @attribute_consuming_services = AttributeConsumingService::Array.new
12
14
  end
13
15
 
16
+ # (see Base#from_xml)
14
17
  def from_xml(node)
15
18
  super
16
19
  @assertion_consumer_services = nil
17
20
  @attribute_consuming_services = nil
18
21
  end
19
22
 
23
+ # @return [Endpoint::Indexed::Array]
20
24
  def assertion_consumer_services
21
25
  @assertion_consumer_services ||= begin
22
26
  nodes = xml.xpath('md:AssertionConsumerService', Namespaces::ALL)
@@ -24,6 +28,7 @@ module SAML2
24
28
  end
25
29
  end
26
30
 
31
+ # @return [AttributeConsumingService::Array]
27
32
  def attribute_consuming_services
28
33
  @attribute_consuming_services ||= begin
29
34
  nodes = xml.xpath('md:AttributeConsumingService', Namespaces::ALL)
@@ -31,6 +36,7 @@ module SAML2
31
36
  end
32
37
  end
33
38
 
39
+ # (see Base#build)
34
40
  def build(builder)
35
41
  builder['md'].SPSSODescriptor do |sp_sso_descriptor|
36
42
  super(sp_sso_descriptor)
@@ -1,7 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'saml2/key'
2
4
 
3
5
  module SAML2
4
6
  module Signable
7
+ # @return [Nokogiri::XML::Element, nil]
5
8
  def signature
6
9
  unless instance_variable_defined?(:@signature)
7
10
  @signature = xml.at_xpath('dsig:Signature', Namespaces::ALL)
@@ -19,14 +22,26 @@ module SAML2
19
22
  @signature
20
23
  end
21
24
 
25
+ # @return [KeyInfo, nil]
22
26
  def signing_key
23
- @signing_key ||= Key.from_xml(signature)
27
+ @signing_key ||= KeyInfo.from_xml(signature)
24
28
  end
25
29
 
26
30
  def signed?
27
31
  !!signature
28
32
  end
29
33
 
34
+ # Validate the signature on this object.
35
+ #
36
+ # Either +fingerprint+ or +cert+ must be provided.
37
+ #
38
+ # @param fingerprint optional [Array<String>, String]
39
+ # SHA1 fingerprints of trusted certificates. If provided, they will be
40
+ # checked against the {#signing_key} embedded in the {#signature}, and if
41
+ # a match is found, the certificate embedded in the signature will be
42
+ # added to the list of certificates used for verifying the signature.
43
+ # @param cert optional [Array<String>, String]
44
+ # @return [Array<String>] An empty array on success, details of errors on failure.
30
45
  def validate_signature(fingerprint: nil, cert: nil, verification_time: nil)
31
46
  return ["not signed"] unless signed?
32
47
 
@@ -34,7 +49,7 @@ module SAML2
34
49
  # see if any given fingerprints match the certificate embedded in the XML;
35
50
  # if so, extract the certificate, and add it to the allowed certificates list
36
51
  Array(fingerprint)&.each do |fp|
37
- certs << signing_key.certificate if signing_key&.fingerprint == Key.format_fingerprint(fp)
52
+ certs << signing_key.certificate if signing_key&.fingerprint == KeyInfo.format_fingerprint(fp)
38
53
  end
39
54
  certs = certs.uniq
40
55
  return ["no certificate found"] if certs.empty?
@@ -49,10 +64,25 @@ module SAML2
49
64
  end
50
65
  end
51
66
 
67
+ # Check if the signature on this object is valid.
68
+ #
69
+ # Either +fingerprint+ or +cert+ must be provided.
70
+ #
71
+ # @param (see #validate_signature)
72
+ # @return [Boolean]
52
73
  def valid_signature?(fingerprint: nil, cert: nil, verification_time: nil)
53
74
  validate_signature(fingerprint: fingerprint, cert: cert, verification_time: verification_time).empty?
54
75
  end
55
76
 
77
+ # Sign this object.
78
+ #
79
+ # @param x509_certificate [String]
80
+ # The certificate corresponding to +private_key+, to be embedded in the
81
+ # signature.
82
+ # @param private_key [String]
83
+ # The key to use to sign.
84
+ # @param algorithm_name [Symbol]
85
+ # @return [self]
56
86
  def sign(x509_certificate, private_key, algorithm_name = :sha256)
57
87
  to_xml
58
88
 
@@ -1,6 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'saml2/role'
2
4
 
3
5
  module SAML2
6
+ # @abstract
4
7
  class SSO < Role
5
8
  def initialize
6
9
  super
@@ -8,21 +11,25 @@ module SAML2
8
11
  @name_id_formats = []
9
12
  end
10
13
 
14
+ # (see Base#from_xml)
11
15
  def from_xml(node)
12
16
  super
13
17
  @single_logout_services = nil
14
18
  @name_id_formats = nil
15
19
  end
16
20
 
21
+ # @return [Array<Endpoint>]
17
22
  def single_logout_services
18
23
  @single_logout_services ||= load_object_array(xml, 'md:SingleLogoutService', Endpoint)
19
24
  end
20
25
 
26
+ # @return [Array<String>]
21
27
  def name_id_formats
22
28
  @name_id_formats ||= load_string_array(xml, 'md:NameIDFormat')
23
29
  end
24
30
 
25
31
  protected
32
+
26
33
  # should be called from inside the role element
27
34
  def build(builder)
28
35
  super
@@ -1,15 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'saml2/base'
2
4
 
3
5
  module SAML2
4
6
  class Status < Base
5
- SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success".freeze
7
+ SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success"
6
8
 
9
+ # @return [String]
7
10
  attr_accessor :code, :message
8
11
 
12
+ # @param code [String]
13
+ # @param message [String, nil]
9
14
  def initialize(code = SUCCESS, message = nil)
10
15
  @code, @message = code, message
11
16
  end
12
17
 
18
+ # (see Base#from_xml)
13
19
  def from_xml(node)
14
20
  super
15
21
  self.code = node.at_xpath('samlp:StatusCode', Namespaces::ALL)['Value']
@@ -20,6 +26,7 @@ module SAML2
20
26
  code == SUCCESS
21
27
  end
22
28
 
29
+ # (see Base#build)
23
30
  def build(builder)
24
31
  builder['samlp'].Status do |status|
25
32
  status['samlp'].StatusCode(Value: code)
@@ -1,21 +1,27 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'saml2/message'
2
4
  require 'saml2/status'
3
5
 
4
6
  module SAML2
5
7
  class StatusResponse < Message
6
- attr_accessor :in_response_to, :status
8
+ # @return [String]
9
+ attr_accessor :in_response_to
10
+ attr_writer :status
7
11
 
8
12
  def initialize
9
13
  super
10
14
  @status = Status.new
11
15
  end
12
16
 
17
+ # (see Base#from_xml)
13
18
  def from_xml(node)
14
19
  super
15
20
  @status = nil
16
21
  remove_instance_variable(:@status)
17
22
  end
18
23
 
24
+ # @return [Status]
19
25
  def status
20
26
  @status ||= Status.from_xml(xml.at_xpath('samlp:Status', Namespaces::ALL))
21
27
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'saml2/name_id'
2
4
  require 'saml2/namespaces'
3
5
 
@@ -10,11 +12,13 @@ module SAML2
10
12
  @confirmations = []
11
13
  end
12
14
 
15
+ # (see Base#from_xml)
13
16
  def from_xml(node)
14
17
  super
15
18
  @confirmations = nil
16
19
  end
17
20
 
21
+ # @return [NameID]
18
22
  def name_id
19
23
  if xml && !instance_variable_defined?(:@name_id)
20
24
  @name_id = NameID.from_xml(xml.at_xpath('saml:NameID', Namespaces::ALL))
@@ -22,18 +26,23 @@ module SAML2
22
26
  @name_id
23
27
  end
24
28
 
29
+ # @return [Confirmation, nil]
25
30
  def confirmation
26
31
  Array.wrap(confirmations).first
27
32
  end
28
33
 
34
+ # @return [Confirmation, nil]
29
35
  def confirmation=(value)
30
- @confirmations = [value]
36
+ @confirmations = value.nil? ? [] : [value]
37
+ confirmation
31
38
  end
32
39
 
40
+ # @return [Confirmation, Array<Confirmation>]
33
41
  def confirmations
34
42
  @confirmations ||= load_object_array(xml, 'saml:SubjectConfirmation', Confirmation)
35
43
  end
36
44
 
45
+ # (see Base#build)
37
46
  def build(builder)
38
47
  builder['saml'].Subject do |subject|
39
48
  name_id.build(subject) if name_id
@@ -45,13 +54,20 @@ module SAML2
45
54
 
46
55
  class Confirmation < Base
47
56
  module Methods
48
- BEARER = 'urn:oasis:names:tc:SAML:2.0:cm:bearer'.freeze
49
- HOLDER_OF_KEY = 'urn:oasis:names:tc:SAML:2.0:cm:holder-of-key'.freeze
50
- SENDER_VOUCHES = 'urn:oasis:names:tc:SAML:2.0:cm:sender-vouches'.freeze
57
+ BEARER = 'urn:oasis:names:tc:SAML:2.0:cm:bearer'
58
+ HOLDER_OF_KEY = 'urn:oasis:names:tc:SAML:2.0:cm:holder-of-key'
59
+ SENDER_VOUCHES = 'urn:oasis:names:tc:SAML:2.0:cm:sender-vouches'
51
60
  end
52
61
 
53
- attr_accessor :method, :not_before, :not_on_or_after, :recipient, :in_response_to
62
+ # @see Methods
63
+ # @return [String]
64
+ attr_accessor :method
65
+ # @return [Time, nil]
66
+ attr_accessor :not_before, :not_on_or_after
67
+ # @return [String, nil]
68
+ attr_accessor :recipient, :in_response_to
54
69
 
70
+ # (see Base#from_xml)
55
71
  def from_xml(node)
56
72
  super
57
73
  self.method = node['Method']
@@ -64,6 +80,7 @@ module SAML2
64
80
  end
65
81
  end
66
82
 
83
+ # (see Base#build)
67
84
  def build(builder)
68
85
  builder['saml'].SubjectConfirmation('Method' => method) do |subject_confirmation|
69
86
  if in_response_to ||
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SAML2
2
- VERSION = '2.0.2'
4
+ VERSION = '2.1.0'
3
5
  end
@@ -146,15 +146,36 @@ module SAML2
146
146
  allow(message).to receive(:destination).and_return("http://somewhere/")
147
147
  allow(message).to receive(:to_s).and_return("hi")
148
148
  key = OpenSSL::PKey::RSA.new(fixture('privatekey.key'))
149
- url = Bindings::HTTPRedirect.encode(message, relay_state: "abc", private_key: key)
149
+ url = Bindings::HTTPRedirect.encode(message,
150
+ relay_state: "abc",
151
+ private_key: key)
150
152
 
151
153
  # verify the signature
152
154
  allow(Message).to receive(:parse).with("hi").and_return("parsed")
153
155
  Bindings::HTTPRedirect.decode(url) do |_message, sig_alg|
154
- expect(sig_alg).not_to be_nil
156
+ expect(sig_alg).to eq Bindings::HTTPRedirect::SigAlgs::RSA_SHA1
155
157
  OpenSSL::X509::Certificate.new(fixture('certificate.pem')).public_key
156
158
  end
157
159
  end
160
+
161
+ it 'signs a message with RSA-SHA256' do
162
+ message = double()
163
+ allow(message).to receive(:destination).and_return("http://somewhere/")
164
+ allow(message).to receive(:to_s).and_return("hi")
165
+ key = OpenSSL::PKey::RSA.new(fixture('privatekey.key'))
166
+ url = Bindings::HTTPRedirect.encode(message,
167
+ relay_state: "abc",
168
+ private_key: key,
169
+ sig_alg: Bindings::HTTPRedirect::SigAlgs::RSA_SHA256)
170
+
171
+ # verify the signature
172
+ allow(Message).to receive(:parse).with("hi").and_return("parsed")
173
+ Bindings::HTTPRedirect.decode(url) do |_message, sig_alg|
174
+ expect(sig_alg).to eq Bindings::HTTPRedirect::SigAlgs::RSA_SHA256
175
+ OpenSSL::X509::Certificate.new(fixture('certificate.pem')).public_key
176
+ end
177
+ end
178
+
158
179
  end
159
180
  end
160
181
  end
@@ -3,13 +3,13 @@ require_relative '../spec_helper'
3
3
  module SAML2
4
4
  describe Conditions do
5
5
  it "empty should be valid" do
6
- expect(Conditions.new.valid?).to eq :valid
6
+ expect(Conditions.new.valid?).to eq true
7
7
  end
8
8
 
9
9
  it "should be valid with unknown condition" do
10
10
  conditions = Conditions.new
11
11
  conditions << Conditions::Condition.new
12
- expect(conditions.valid?).to eq :indeterminate
12
+ expect(conditions.valid?).to eq nil
13
13
  end
14
14
 
15
15
  it "should be valid with timestamps" do
@@ -17,7 +17,7 @@ module SAML2
17
17
  now = Time.now.utc
18
18
  conditions.not_before = now - 5
19
19
  conditions.not_on_or_after = now + 30
20
- expect(conditions.valid?).to eq :valid
20
+ expect(conditions.valid?).to eq true
21
21
  end
22
22
 
23
23
  it "should be invalid with out of range timestamps" do
@@ -25,7 +25,7 @@ module SAML2
25
25
  now = Time.now.utc
26
26
  conditions.not_before = now - 35
27
27
  conditions.not_on_or_after = now - 5
28
- expect(conditions.valid?).to eq :invalid
28
+ expect(conditions.valid?).to eq false
29
29
  end
30
30
 
31
31
  it "should allow passing now" do
@@ -33,7 +33,7 @@ module SAML2
33
33
  now = Time.now.utc
34
34
  conditions.not_before = now - 35
35
35
  conditions.not_on_or_after = now - 5
36
- expect(conditions.valid?(now: now - 10)).to eq :valid
36
+ expect(conditions.valid?(now: now - 10)).to eq true
37
37
  end
38
38
 
39
39
  it "should be invalid before indeterminate" do
@@ -41,29 +41,28 @@ module SAML2
41
41
  now = Time.now.utc
42
42
  conditions.not_before = now + 5
43
43
  conditions << Conditions::Condition.new
44
- expect(conditions.valid?).to eq :invalid
44
+ expect(conditions.valid?).to eq false
45
45
  end
46
46
 
47
47
  it "should be invalid before indeterminate (actual conditions)" do
48
48
  conditions = Conditions.new
49
49
  conditions << Conditions::Condition.new
50
50
  conditions << Conditions::AudienceRestriction.new('audience')
51
- expect(conditions.valid?).to eq :invalid
51
+ expect(conditions.valid?).to eq false
52
52
  end
53
-
54
53
  end
55
54
 
56
55
  describe Conditions::AudienceRestriction do
57
56
  it "should be invalid" do
58
- expect(Conditions::AudienceRestriction.new('expected').valid?(audience: 'actual')).to eq :invalid
57
+ expect(Conditions::AudienceRestriction.new('expected').valid?(audience: 'actual')).to eq false
59
58
  end
60
59
 
61
60
  it "should be valid" do
62
- expect(Conditions::AudienceRestriction.new('expected').valid?(audience: 'expected')).to eq :valid
61
+ expect(Conditions::AudienceRestriction.new('expected').valid?(audience: 'expected')).to eq true
63
62
  end
64
63
 
65
64
  it "should be valid with an array" do
66
- expect(Conditions::AudienceRestriction.new(['expected', 'actual']).valid?(audience: 'actual')).to eq :valid
65
+ expect(Conditions::AudienceRestriction.new(['expected', 'actual']).valid?(audience: 'actual')).to eq true
67
66
  end
68
67
  end
69
68
  end
@@ -14,7 +14,7 @@ module SAML2
14
14
  idp = IdentityProvider.new
15
15
  idp.name_id_formats << NameID::Format::PERSISTENT
16
16
  idp.single_sign_on_services << Endpoint.new('https://sso.canvaslms.com/SAML2/Login')
17
- idp.keys << Key.new('somedata', Key::Type::SIGNING)
17
+ idp.keys << KeyDescriptor.new('somedata', KeyDescriptor::Type::SIGNING)
18
18
 
19
19
  entity.roles << idp
20
20
  expect(Schemas.metadata.validate(Nokogiri::XML(entity.to_s))).to eq []
@@ -16,8 +16,8 @@ module SAML2
16
16
  Bindings::HTTPRedirect::URN)
17
17
  sp.assertion_consumer_services << Endpoint::Indexed.new('https://sso.canvaslms.com/SAML2/Login1')
18
18
  sp.assertion_consumer_services << Endpoint::Indexed.new('https://sso.canvaslms.com/SAML2/Login2')
19
- sp.keys << Key.new('somedata', Key::Type::ENCRYPTION, [Key::EncryptionMethod.new])
20
- sp.keys << Key.new('somedata', Key::Type::SIGNING)
19
+ sp.keys << KeyDescriptor.new('somedata', KeyDescriptor::Type::ENCRYPTION, [KeyDescriptor::EncryptionMethod.new])
20
+ sp.keys << KeyDescriptor.new('somedata', KeyDescriptor::Type::SIGNING)
21
21
  acs = AttributeConsumingService.new
22
22
  acs.name[:en] = 'service'
23
23
  acs.requested_attributes << RequestedAttribute.create('uid')
@@ -59,6 +59,11 @@ module SAML2
59
59
  expect(acs.requested_attributes.length).to eq 1
60
60
  expect(acs.requested_attributes.first.name).to eq 'urn:oid:2.5.4.42'
61
61
  end
62
+
63
+ it "loads the key info" do
64
+ expect(sp.keys.first.encryption_methods.first.algorithm).to eq KeyDescriptor::EncryptionMethod::Algorithm::AES128_CBC
65
+ expect(sp.keys.first.encryption_methods.first.key_size).to eq 128
66
+ end
62
67
  end
63
68
  end
64
69
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: saml2
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.2
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-02-17 00:00:00.000000000 Z
11
+ date: 2018-03-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -31,7 +31,7 @@ dependencies:
31
31
  - !ruby/object:Gem::Version
32
32
  version: '1.9'
33
33
  - !ruby/object:Gem::Dependency
34
- name: nokogiri-xmlsec-me-harder
34
+ name: nokogiri-xmlsec-instructure
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
@@ -39,7 +39,7 @@ dependencies:
39
39
  version: '0.9'
40
40
  - - ">="
41
41
  - !ruby/object:Gem::Version
42
- version: 0.9.3pre
42
+ version: 0.9.4
43
43
  type: :runtime
44
44
  prerelease: false
45
45
  version_requirements: !ruby/object:Gem::Requirement
@@ -49,7 +49,7 @@ dependencies:
49
49
  version: '0.9'
50
50
  - - ">="
51
51
  - !ruby/object:Gem::Version
52
- version: 0.9.3pre
52
+ version: 0.9.4
53
53
  - !ruby/object:Gem::Dependency
54
54
  name: activesupport
55
55
  requirement: !ruby/object:Gem::Requirement