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,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