saml2 2.0.2 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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