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,23 +1,32 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'saml2/bindings/http_post'
2
4
 
3
5
  module SAML2
4
6
  class Endpoint < Base
7
+ # @return [String]
5
8
  attr_reader :location, :binding
6
9
 
10
+ # @param location [String]
11
+ # @param binding [String]
7
12
  def initialize(location = nil, binding = Bindings::HTTP_POST::URN)
8
13
  @location, @binding = location, binding
9
14
  end
10
15
 
16
+ # @param rhs [Endpoint]
17
+ # @return [Boolean]
11
18
  def ==(rhs)
12
19
  location == rhs.location && binding == rhs.binding
13
20
  end
14
21
 
22
+ # (see Base#from_xml)
15
23
  def from_xml(node)
16
24
  super
17
25
  @location = node['Location']
18
26
  @binding = node['Binding']
19
27
  end
20
28
 
29
+ # (see Base#build)
21
30
  def build(builder, element)
22
31
  builder['md'].__send__(element, 'Location' => location, 'Binding' => binding)
23
32
  end
@@ -25,6 +34,10 @@ module SAML2
25
34
  class Indexed < Endpoint
26
35
  include IndexedObject
27
36
 
37
+ # @param location [String]
38
+ # @param index [Integer]
39
+ # @param is_default [true, false, nil]
40
+ # @param binding [String]
28
41
  def initialize(location = nil, index = nil, is_default = nil, binding = Bindings::HTTP_POST::URN)
29
42
  super(location, binding)
30
43
  @index, @is_default = index, is_default
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SAML2
2
4
  class Engine < Rails::Engine
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'nokogiri'
2
4
 
3
5
  require 'saml2/base'
@@ -11,8 +13,13 @@ module SAML2
11
13
  include OrganizationAndContacts
12
14
  include Signable
13
15
 
16
+ # @return [String]
14
17
  attr_writer :entity_id
15
18
 
19
+ # Parse a metadata file, and return an appropriate object.
20
+ #
21
+ # @param xml [String, IO] Anything that can be passed to +Nokogiri::XML+
22
+ # @return [Entity, Group, nil]
16
23
  def self.parse(xml)
17
24
  document = Nokogiri::XML(xml)
18
25
 
@@ -46,6 +53,7 @@ module SAML2
46
53
  @valid_until = nil
47
54
  end
48
55
 
56
+ # (see Base#from_xml)
49
57
  def from_xml(node)
50
58
  super
51
59
  @id = nil
@@ -55,14 +63,17 @@ module SAML2
55
63
  'EntitiesDescriptor' => Group)
56
64
  end
57
65
 
66
+ # (see Message#valid_schema?)
58
67
  def valid_schema?
59
68
  Schemas.federation.valid?(xml.document)
60
69
  end
61
70
 
71
+ # (see Message#id)
62
72
  def id
63
73
  @id ||= xml['ID']
64
74
  end
65
75
 
76
+ # @return [Time, nil]
66
77
  def valid_until
67
78
  unless instance_variable_defined?(:@valid_until)
68
79
  @valid_until = xml['validUntil'] && Time.parse(xml['validUntil'])
@@ -79,6 +90,7 @@ module SAML2
79
90
  @id = "_#{SecureRandom.uuid}"
80
91
  end
81
92
 
93
+ # (see Base#from_xml)
82
94
  def from_xml(node)
83
95
  super
84
96
  @id = nil
@@ -86,18 +98,22 @@ module SAML2
86
98
  @roles = nil
87
99
  end
88
100
 
101
+ # (see Message#valid_schema?)
89
102
  def valid_schema?
90
103
  Schemas.federation.valid?(xml.document)
91
104
  end
92
105
 
106
+ # @return [String]
93
107
  def entity_id
94
108
  @entity_id || xml && xml['entityID']
95
109
  end
96
110
 
111
+ # (see Message#id)
97
112
  def id
98
113
  @id ||= xml['ID']
99
114
  end
100
115
 
116
+ # @return [Time, nil]
101
117
  def valid_until
102
118
  unless instance_variable_defined?(:@valid_until)
103
119
  @valid_until = xml['validUntil'] && Time.parse(xml['validUntil'])
@@ -105,19 +121,23 @@ module SAML2
105
121
  @valid_until
106
122
  end
107
123
 
124
+ # @return [Array<IdentityProvider>]
108
125
  def identity_providers
109
126
  roles.select { |r| r.is_a?(IdentityProvider) }
110
127
  end
111
128
 
129
+ # @return [Array<ServiceProvider>]
112
130
  def service_providers
113
131
  roles.select { |r| r.is_a?(ServiceProvider) }
114
132
  end
115
133
 
134
+ # @return [Array<Role>]
116
135
  def roles
117
136
  @roles ||= load_object_array(xml, 'md:IDPSSODescriptor', IdentityProvider) +
118
137
  load_object_array(xml, 'md:SPSSODescriptor', ServiceProvider)
119
138
  end
120
139
 
140
+ # (see Base#build)
121
141
  def build(builder)
122
142
  builder['md'].EntityDescriptor('entityID' => entity_id,
123
143
  'xmlns:md' => Namespaces::METADATA,
@@ -1,9 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'saml2/attribute'
2
4
  require 'saml2/sso'
3
5
 
4
6
  module SAML2
5
7
  class IdentityProvider < SSO
6
- attr_writer :want_authn_requests_signed, :single_sign_on_services, :attribute_profiles, :attributes
8
+ # @return [Boolean, nil]
9
+ attr_writer :want_authn_requests_signed
10
+ attr_writer :single_sign_on_services, :attribute_profiles, :attributes
7
11
 
8
12
  def initialize
9
13
  super
@@ -13,6 +17,7 @@ module SAML2
13
17
  @attributes = []
14
18
  end
15
19
 
20
+ # (see Base#from_xml)
16
21
  def from_xml(node)
17
22
  super
18
23
  remove_instance_variable(:@want_authn_requests_signed)
@@ -21,6 +26,7 @@ module SAML2
21
26
  @attributes = nil
22
27
  end
23
28
 
29
+ # @return [Boolean, nil]
24
30
  def want_authn_requests_signed?
25
31
  unless instance_variable_defined?(:@want_authn_requests_signed)
26
32
  @want_authn_requests_signed = xml['WantAuthnRequestsSigned'] && xml['WantAuthnRequestsSigned'] == 'true'
@@ -28,18 +34,22 @@ module SAML2
28
34
  @want_authn_requests_signed
29
35
  end
30
36
 
37
+ # @return [Array<Endpoint>]
31
38
  def single_sign_on_services
32
39
  @single_sign_on_services ||= load_object_array(xml, 'md:SingleSignOnService', Endpoint)
33
40
  end
34
41
 
42
+ # @return [Array<String>]
35
43
  def attribute_profiles
36
44
  @attribute_profiles ||= load_string_array(xml, 'md:AttributeProfile')
37
45
  end
38
46
 
47
+ # @return [Array<Attribute>]
39
48
  def attributes
40
49
  @attributes ||= load_object_array(xml, 'saml:Attribute', Attribute)
41
50
  end
42
51
 
52
+ # (see Base#build)
43
53
  def build(builder)
44
54
  builder['md'].IDPSSODescriptor do |idp_sso_descriptor|
45
55
  super(idp_sso_descriptor)
@@ -1,18 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'saml2/base'
2
4
 
3
5
  module SAML2
4
6
  module IndexedObject
7
+ # @return [Integer]
5
8
  attr_accessor :index
6
9
 
7
- def initialize(*args)
10
+ def initialize(*)
8
11
  @is_default = nil
9
12
  super
10
13
  end
11
14
 
12
15
  def eql?(rhs)
13
16
  index == rhs.index &&
14
- default? == rhs.default? &&
15
- super
17
+ default? == rhs.default? &&
18
+ super
16
19
  end
17
20
 
18
21
  def default?
@@ -23,13 +26,18 @@ module SAML2
23
26
  !@is_default.nil?
24
27
  end
25
28
 
29
+ # (see Base#from_xml)
26
30
  def from_xml(node)
27
31
  @index = node['index'] && node['index'].to_i
28
32
  @is_default = node['isDefault'] && node['isDefault'] == 'true'
29
33
  super
30
34
  end
31
35
 
36
+ # Keeps an Array of {IndexedObject}s in their +index+ed order.
32
37
  class Array < ::Array
38
+ # Returns the first object which is set as the default, or the first
39
+ # object if none are set as the default.
40
+ # @return [IndexedObject]
33
41
  attr_reader :default
34
42
 
35
43
  def self.from_xml(nodes)
@@ -68,6 +76,7 @@ module SAML2
68
76
  end
69
77
  end
70
78
 
79
+ # (see Base#build)
71
80
  def build(builder, *)
72
81
  super
73
82
  builder.parent.children.last['index'] = index
@@ -75,6 +84,7 @@ module SAML2
75
84
  end
76
85
 
77
86
  private
87
+
78
88
  def self.included(klass)
79
89
  klass.const_set(:Array, Array.dup)
80
90
  end
@@ -1,23 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'saml2/base'
1
4
  require 'saml2/namespaces'
2
5
 
3
6
  module SAML2
4
- class Key
7
+ # This represents the XML Signatures <KeyInfo> element, and actually contains a
8
+ # reference to an X.509 certificate, not solely a public key.
9
+ class KeyInfo < Base
10
+ # @return [String] The PEM encoded certificate.
11
+ attr_reader :x509
12
+
13
+ # @param x509 [String] The PEM encoded certificate.
14
+ def initialize(x509 = nil)
15
+ self.x509 = x509
16
+ end
17
+
18
+ # (see Base#from_xml)
19
+ def from_xml(node)
20
+ self.x509 = node.at_xpath('dsig:KeyInfo/dsig:X509Data/dsig:X509Certificate', Namespaces::ALL)&.content&.strip
21
+ end
22
+
23
+ def x509=(value)
24
+ @x509 = value&.gsub(/\w*-+(BEGIN|END) CERTIFICATE-+\w*/, "")&.strip
25
+ end
26
+
27
+ # @return [OpenSSL::X509::Certificate]
28
+ def certificate
29
+ @certificate ||= OpenSSL::X509::Certificate.new(Base64.decode64(x509))
30
+ end
31
+
32
+ # Formats a fingerprint as all lowercase, with a : every two characters.
33
+ # @param fingerprint [String]
34
+ # @return [String]
35
+ def self.format_fingerprint(fingerprint)
36
+ fingerprint.downcase.gsub(/(\h{2})(?=\h)/, '\1:')
37
+ end
38
+
39
+ # @return [String]
40
+ def fingerprint
41
+ @fingerprint ||= self.class.format_fingerprint(Digest::SHA1.hexdigest(certificate.to_der))
42
+ end
43
+
44
+ # (see Base#build)
45
+ def build(builder)
46
+ builder['dsig'].KeyInfo do |key_info|
47
+ key_info['dsig'].X509Data do |x509_data|
48
+ x509_data['dsig'].X509Certificate(x509)
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ class KeyDescriptor < KeyInfo
5
55
  module Type
6
- ENCRYPTION = 'encryption'.freeze
7
- SIGNING = 'signing'.freeze
56
+ ENCRYPTION = 'encryption'
57
+ SIGNING = 'signing'
8
58
  end
9
59
 
10
- class EncryptionMethod
60
+ class EncryptionMethod < Base
11
61
  module Algorithm
12
- AES128_CBC = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'.freeze
62
+ AES128_CBC = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'
13
63
  end
14
64
 
15
- attr_accessor :algorithm, :key_size
65
+ # @see Algorithm
66
+ # @return [String]
67
+ attr_accessor :algorithm
68
+ # @return [Integer]
69
+ attr_accessor :key_size
16
70
 
71
+ # @param algorithm [String]
72
+ # @param key_size [Integer]
17
73
  def initialize(algorithm = Algorithm::AES128_CBC, key_size = 128)
18
74
  @algorithm, @key_size = algorithm, key_size
19
75
  end
20
76
 
77
+ # (see Base#from_xml)
78
+ def from_xml(node)
79
+ self.algorithm = node['Algorithm']
80
+ self.key_size = node.at_xpath('xenc:KeySize', Namespaces::ALL)&.content&.to_i
81
+ end
82
+
83
+ # (see Base#build)
21
84
  def build(builder)
22
85
  builder['md'].EncryptionMethod('Algorithm' => algorithm) do |encryption_method|
23
86
  encryption_method['xenc'].KeySize(key_size) if key_size
@@ -25,18 +88,24 @@ module SAML2
25
88
  end
26
89
  end
27
90
 
28
- attr_accessor :use, :x509, :encryption_methods
91
+ # @see Type
92
+ # @return [String]
93
+ attr_accessor :use
94
+ # @return [Array<EncryptionMethod>]
95
+ attr_accessor :encryption_methods
29
96
 
30
- def self.from_xml(node)
31
- return nil unless node
32
-
33
- x509 = node.at_xpath('dsig:KeyInfo/dsig:X509Data/dsig:X509Certificate', Namespaces::ALL)
34
- methods = node.xpath('xenc:EncryptionMethod', Namespaces::ALL)
35
- new(x509 && x509.content.strip, node['use'], methods.map { |m| m['Algorithm'] })
97
+ # (see Base#from_xml)
98
+ def from_xml(node)
99
+ super
100
+ self.use = node['use']
101
+ self.encryption_methods = load_object_array(node, 'md:EncryptionMethod', EncryptionMethod)
36
102
  end
37
103
 
38
- def initialize(x509, use = nil, encryption_methods = [])
39
- @use, @x509, @encryption_methods = use, x509.gsub(/\w*-+(BEGIN|END) CERTIFICATE-+\w*/, "").strip, encryption_methods
104
+ # @param x509 [String] The PEM encoded certificate.
105
+ # @param use optional [String] See {Type}
106
+ # @param encryption_methods [Array<EncryptionMethod>]
107
+ def initialize(x509 = nil, use = nil, encryption_methods = [])
108
+ @use, self.x509, @encryption_methods = use, x509, encryption_methods
40
109
  end
41
110
 
42
111
  def encryption?
@@ -47,30 +116,18 @@ module SAML2
47
116
  use.nil? || use == Type::SIGNING
48
117
  end
49
118
 
50
- def certificate
51
- @certificate ||= OpenSSL::X509::Certificate.new(Base64.decode64(x509))
52
- end
53
-
54
- def self.format_fingerprint(fingerprint)
55
- fingerprint.downcase.gsub(/(\h{2})(?=\h)/, '\1:')
56
- end
57
-
58
- def fingerprint
59
- @fingerprint ||= self.class.format_fingerprint(Digest::SHA1.hexdigest(certificate.to_der))
60
- end
61
-
119
+ # (see Base#build)
62
120
  def build(builder)
63
121
  builder['md'].KeyDescriptor do |key_descriptor|
64
122
  key_descriptor.parent['use'] = use if use
65
- key_descriptor['dsig'].KeyInfo do |key_info|
66
- key_info['dsig'].X509Data do |x509_data|
67
- x509_data['dsig'].X509Certificate(x509)
68
- end
69
- end
123
+ super(key_descriptor)
70
124
  encryption_methods.each do |method|
71
125
  method.build(key_descriptor)
72
126
  end
73
127
  end
74
128
  end
75
129
  end
130
+
131
+ # @deprecated Deprecated alias for KeyDescriptor
132
+ Key = KeyDescriptor
76
133
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'saml2/base'
2
4
  require 'saml2/namespaces'
3
5
 
@@ -16,6 +18,11 @@ module SAML2
16
18
  end
17
19
  end
18
20
 
21
+ # @param lang [String, Symbol, :all, nil]
22
+ # The language to retrieve the localized string for.
23
+ # +:all+ will return the hash itself, and +nil+ will return the first
24
+ # localized string regardless of language.
25
+ # @return [String]
19
26
  def [](lang)
20
27
  case lang
21
28
  when :all
@@ -27,6 +34,7 @@ module SAML2
27
34
  end
28
35
  end
29
36
 
37
+ # @return [String] The first localized string regardless of language
30
38
  def to_s
31
39
  self[nil].to_s
32
40
  end
@@ -1,11 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'saml2/name_id'
2
4
  require 'saml2/request'
3
5
 
4
6
  module SAML2
5
7
  class LogoutRequest < Request
6
- attr_accessor :name_id, :session_index
7
-
8
- def self.initiate(sso, issuer, name_id, session_index = nil)
8
+ attr_writer :name_id, :session_index
9
+
10
+ # @param sso [SSO]
11
+ # @param issuer [NameID]
12
+ # @param name_id [NameID]
13
+ # @param session_index optional [String, Array<String>]
14
+ # @return [LogoutRequest]
15
+ def self.initiate(sso, issuer, name_id, session_index = [])
9
16
  logout_request = new
10
17
  logout_request.issuer = issuer
11
18
  logout_request.destination = sso.single_logout_services.first.location
@@ -15,10 +22,12 @@ module SAML2
15
22
  logout_request
16
23
  end
17
24
 
25
+ # @return [NameID]
18
26
  def name_id
19
27
  @name_id ||= (NameID.from_xml(xml.at_xpath('saml:NameID', Namespaces::ALL)) if xml)
20
28
  end
21
29
 
30
+ # @return [String, Array<String>]
22
31
  def session_index
23
32
  @session_index ||= (load_string_array(xml,'samlp:SessionIndex') if xml)
24
33
  end