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,7 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'saml2/status_response'
2
4
 
3
5
  module SAML2
4
6
  class LogoutResponse < StatusResponse
7
+ # @param logout_request [LogoutRequest]
8
+ # @param sso [SSO]
9
+ # @param issuer [NameID]
10
+ # @param status_code [String]
11
+ # @return [LogoutResponse]
5
12
  def self.respond_to(logout_request, sso, issuer, status_code = Status::SUCCESS)
6
13
  logout_response = new
7
14
  logout_response.issuer = issuer
@@ -11,6 +18,8 @@ module SAML2
11
18
  logout_response
12
19
  end
13
20
 
21
+ private
22
+
14
23
  def build(builder)
15
24
  builder['samlp'].LogoutResponse(
16
25
  'xmlns:samlp' => Namespaces::SAMLP,
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'securerandom'
2
4
  require 'time'
3
5
 
@@ -37,18 +39,23 @@ module SAML2
37
39
 
38
40
  # In the SAML Schema, Request and Response don't technically share a common
39
41
  # ancestor, but they have several things in common so it's useful to represent
40
- # that here
42
+ # that in this gem as a common base class.
43
+ # @abstract
41
44
  class Message < Base
42
45
  include Signable
43
46
 
44
47
  attr_writer :issuer, :destination
45
48
 
46
49
  class << self
47
- def inherited(klass)
48
- # explicitly keep track of all messages in this base class
49
- Message.known_messages[klass.name.sub(/^SAML2::/, '')] = klass
50
- end
51
-
50
+ # Create an appropriate {Message} subclass instance to represent the
51
+ # given XML element.
52
+ #
53
+ # When called on a subclass, it behaves the same as {Base.from_xml}
54
+ #
55
+ # @param node [Nokogiri::XML::Element]
56
+ # @return [Message]
57
+ # @raise [UnknownMessage] If the element doesn't correspond to a known
58
+ # SAML message type.
52
59
  def from_xml(node)
53
60
  return super unless self == Message
54
61
  klass = Message.known_messages[node.name]
@@ -56,6 +63,13 @@ module SAML2
56
63
  klass.from_xml(node)
57
64
  end
58
65
 
66
+ # Parses XML, and returns an appropriate {Message} subclass instance.
67
+ #
68
+ # @param xml [String, IO] Anything that can be passed to +Nokogiri::XML+.
69
+ # @return [Message]
70
+ # @raise [UnexpectedMessage]
71
+ # If called on a subclass, will raise if the parsed message does not
72
+ # match the class is was called on.
59
73
  def parse(xml)
60
74
  result = Message.from_xml(Nokogiri::XML(xml) { |config| config.strict }.root)
61
75
  raise UnexpectedMessage.new("Expected a #{self.name}, but got a #{result.class.name}") unless self == Message || result.class == self
@@ -69,6 +83,12 @@ module SAML2
69
83
  def known_messages
70
84
  @known_messages ||= {}
71
85
  end
86
+
87
+ def inherited(klass)
88
+ # explicitly keep track of all messages in this base class
89
+ Message.known_messages[klass.name.sub(/^SAML2::/, '')] = klass
90
+ end
91
+
72
92
  end
73
93
 
74
94
  def initialize
@@ -77,23 +97,30 @@ module SAML2
77
97
  @issue_instant = Time.now.utc
78
98
  end
79
99
 
100
+ # (see Base#from_xml)
80
101
  def from_xml(node)
81
102
  super
82
103
  @id = nil
83
104
  @issue_instant = nil
84
105
  end
85
106
 
107
+ # If the XML is valid according to SAML XSDs.
108
+ # @return [Boolean]
86
109
  def valid_schema?
87
110
  return false unless Schemas.protocol.valid?(xml.document)
88
111
 
89
112
  true
90
113
  end
91
114
 
92
- def validate_signature(fingerprint: nil, cert: nil, verification_time: nil)
115
+ # (see Signable#validate_signature)
116
+ # @param verification_time
117
+ # Ignored. The message's {issue_instant} is always used.
118
+ def validate_signature(fingerprint: nil, cert: nil, verification_time: issue_instant)
93
119
  # verify the signature (certificate's validity) as of the time the message was generated
94
120
  super(fingerprint: fingerprint, cert: cert, verification_time: issue_instant)
95
121
  end
96
122
 
123
+ # (see Signable#sign)
97
124
  def sign(x509_certificate, private_key, algorithm_name = :sha256)
98
125
  super
99
126
 
@@ -105,14 +132,17 @@ module SAML2
105
132
  self
106
133
  end
107
134
 
135
+ # @return [String]
108
136
  def id
109
137
  @id ||= xml['ID']
110
138
  end
111
139
 
140
+ # @return [Time]
112
141
  def issue_instant
113
142
  @issue_instant ||= Time.parse(xml['IssueInstant'])
114
143
  end
115
144
 
145
+ # @return [String, nil]
116
146
  def destination
117
147
  if xml && !instance_variable_defined?(:@destination)
118
148
  @destination = xml['Destination']
@@ -120,6 +150,7 @@ module SAML2
120
150
  @destination
121
151
  end
122
152
 
153
+ # @return [NameID, nil]
123
154
  def issuer
124
155
  @issuer ||= NameID.from_xml(xml.at_xpath('saml:Issuer', Namespaces::ALL))
125
156
  end
@@ -1,27 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'saml2/base'
1
4
  require 'saml2/namespaces'
2
5
 
3
6
  module SAML2
4
- class NameID
7
+ class NameID < Base
5
8
  module Format
6
- EMAIL_ADDRESS = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress".freeze
7
- ENTITY = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity".freeze
8
- KERBEROS = "urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos".freeze # name[/instance]@REALM
9
- PERSISTENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent".freeze # opaque, pseudo-random, unique per SP-IdP pair
10
- TRANSIENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient".freeze # opaque, will likely change
11
- UNSPECIFIED = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified".freeze
12
- WINDOWS_DOMAIN_QUALIFIED_NAME = "urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName".freeze # [DomainName\]UserName
13
- X509_SUBJECT_NAME = "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName".freeze
9
+ EMAIL_ADDRESS = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
10
+ ENTITY = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
11
+ KERBEROS = "urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos" # name[/instance]@REALM
12
+ PERSISTENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" # opaque, pseudo-random, unique per SP-IdP pair
13
+ TRANSIENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" # opaque, will likely change
14
+ UNSPECIFIED = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
15
+ WINDOWS_DOMAIN_QUALIFIED_NAME = "urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName" # [DomainName\]UserName
16
+ X509_SUBJECT_NAME = "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName"
14
17
  end
15
18
 
16
19
  class Policy < Base
17
- attr_writer :allow_create, :format, :sp_name_qualifier
20
+ # @return [Boolean, nil]
21
+ attr_writer :allow_create
22
+ attr_writer :format, :sp_name_qualifier
18
23
 
24
+ # @param allow_create optional [Boolean]
25
+ # @param format optional [String]
26
+ # @param sp_name_qualifier optional [String]
19
27
  def initialize(allow_create = nil, format = nil, sp_name_qualifier = nil)
20
28
  @allow_create = allow_create if allow_create
21
29
  @format = format if format
22
30
  @sp_name_qualifier = sp_name_qualifier if sp_name_qualifier
23
31
  end
24
32
 
33
+ # @return [Boolean, nil]
25
34
  def allow_create?
26
35
  if xml && !instance_variable_defined?(:@allow_create)
27
36
  @allow_create = xml['AllowCreate']&.== 'true'
@@ -29,6 +38,8 @@ module SAML2
29
38
  @allow_create
30
39
  end
31
40
 
41
+ # @see Format
42
+ # @return [String, nil]
32
43
  def format
33
44
  if xml && !instance_variable_defined?(:@format)
34
45
  @format = xml['Format']
@@ -36,6 +47,7 @@ module SAML2
36
47
  @format
37
48
  end
38
49
 
50
+ # @return [String, nil]
39
51
  def sp_name_qualifier
40
52
  if xml && !instance_variable_defined?(:@sp_name_qualifier)
41
53
  @sp_name_qualifier = xml['SPNameQualifier']
@@ -43,12 +55,15 @@ module SAML2
43
55
  @sp_name_qualifier
44
56
  end
45
57
 
58
+ # @param rhs [Policy]
59
+ # @return [Boolean]
46
60
  def ==(rhs)
47
61
  allow_create? == rhs.allow_create? &&
48
62
  format == rhs.format &&
49
63
  sp_name_qualifier == rhs.sp_name_qualifier
50
64
  end
51
65
 
66
+ # (see Base#build)
52
67
  def build(builder)
53
68
  builder['samlp'].NameIDPolicy do |name_id_policy|
54
69
  name_id_policy.parent['Format'] = format if format
@@ -58,20 +73,30 @@ module SAML2
58
73
  end
59
74
  end
60
75
 
61
- attr_accessor :id, :format, :name_qualifier, :sp_name_qualifier
76
+ # @return [String]
77
+ attr_accessor :id
78
+ # @return [String, nil]
79
+ attr_accessor :format, :name_qualifier, :sp_name_qualifier
62
80
 
63
- def self.from_xml(node)
64
- node && new(node.content.strip,
65
- node['Format'],
66
- name_qualifier: node['NameQualifier'],
67
- sp_name_qualifier: node['SPNameQualifier'])
81
+ # (see Base#from_xml)
82
+ def from_xml(node)
83
+ self.id = node.content.strip
84
+ self.format = node['Format']
85
+ self.name_qualifier = node['NameQualifier']
86
+ self.sp_name_qualifier = node['SPNameQualifier']
68
87
  end
69
88
 
89
+ # @param id [String]
90
+ # @param format optional [String]
91
+ # @param name_qualifier optional [String]
92
+ # @param sp_name_qualifier optional [String]
70
93
  def initialize(id = nil, format = nil, name_qualifier: nil, sp_name_qualifier: nil)
71
94
  @id, @format, @name_qualifier, @sp_name_qualifier =
72
95
  id, format, name_qualifier, sp_name_qualifier
73
96
  end
74
97
 
98
+ # @param rhs [NameID]
99
+ # @return [Boolean]
75
100
  def ==(rhs)
76
101
  id == rhs.id &&
77
102
  format == rhs.format &&
@@ -79,6 +104,7 @@ module SAML2
79
104
  sp_name_qualifier == rhs.sp_name_qualifier
80
105
  end
81
106
 
107
+ # (see Base#build)
82
108
  def build(builder, element: nil)
83
109
  args = {}
84
110
  args['Format'] = format if format
@@ -1,13 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SAML2
2
4
  module Namespaces
3
- DSIG = "http://www.w3.org/2000/09/xmldsig#".freeze
4
- METADATA = "urn:oasis:names:tc:SAML:2.0:metadata".freeze
5
- SAML = "urn:oasis:names:tc:SAML:2.0:assertion".freeze
6
- SAMLP = "urn:oasis:names:tc:SAML:2.0:protocol".freeze
7
- XENC = "http://www.w3.org/2001/04/xmlenc#".freeze
8
- XS = "http://www.w3.org/2001/XMLSchema".freeze
9
- XSI = "http://www.w3.org/2001/XMLSchema-instance".freeze
10
- X500 = "urn:oasis:names:tc:SAML:2.0:profiles:attribute:X500".freeze
5
+ DSIG = "http://www.w3.org/2000/09/xmldsig#"
6
+ METADATA = "urn:oasis:names:tc:SAML:2.0:metadata"
7
+ SAML = "urn:oasis:names:tc:SAML:2.0:assertion"
8
+ SAMLP = "urn:oasis:names:tc:SAML:2.0:protocol"
9
+ XENC = "http://www.w3.org/2001/04/xmlenc#"
10
+ XS = "http://www.w3.org/2001/XMLSchema"
11
+ XSI = "http://www.w3.org/2001/XMLSchema-instance"
12
+ X500 = "urn:oasis:names:tc:SAML:2.0:profiles:attribute:X500"
11
13
 
12
14
  ALL = {
13
15
  'xmlns:dsig' => DSIG,
@@ -1,11 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'saml2/base'
2
4
  require 'saml2/localized_name'
3
5
  require 'saml2/namespaces'
4
6
 
5
7
  module SAML2
6
8
  class Organization < Base
9
+ # @return [LocalizedName]
7
10
  attr_reader :name, :display_name, :url
8
11
 
12
+ # (see Base#from_xml)
9
13
  def from_xml(node)
10
14
  name.from_xml(node.xpath('md:OrganizationName', Namespaces::ALL))
11
15
  display_name.from_xml(node.xpath('md:OrganizationDisplayName', Namespaces::ALL))
@@ -18,6 +22,7 @@ module SAML2
18
22
  @url = LocalizedName.new('OrganizationURL', url)
19
23
  end
20
24
 
25
+ # (see Base#build)
21
26
  def build(builder)
22
27
  builder['md'].Organization do |organization|
23
28
  @name.build(organization)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'saml2/contact'
2
4
  require 'saml2/organization'
3
5
 
@@ -10,12 +12,14 @@ module SAML2
10
12
  @contacts = []
11
13
  end
12
14
 
15
+ # (see Base#from_xml)
13
16
  def from_xml(node)
14
17
  remove_instance_variable(:@organization)
15
18
  @contacts = nil
16
19
  super
17
20
  end
18
21
 
22
+ # @return [Organization, nil]
19
23
  def organization
20
24
  unless instance_variable_defined?(:@organization)
21
25
  @organization = Organization.from_xml(xml.at_xpath('md:Organization', Namespaces::ALL))
@@ -23,6 +27,7 @@ module SAML2
23
27
  @organization
24
28
  end
25
29
 
30
+ # @return [Array<Contact>]
26
31
  def contacts
27
32
  @contacts ||= load_object_array(xml, 'md:ContactPerson', Contact)
28
33
  end
@@ -1,8 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'saml2/message'
2
4
  require 'saml2/name_id'
3
5
  require 'saml2/namespaces'
4
6
 
5
7
  module SAML2
8
+ # @abstract
6
9
  class Request < Message
7
10
  end
8
11
  end
@@ -1,9 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'saml2/base'
2
4
 
3
5
  module SAML2
4
6
  class RequestedAuthnContext < Base
5
- attr_accessor :comparison, :class_ref
7
+ # @return [String, nil]
8
+ attr_accessor :comparison
9
+ # @return [String, Array<String>]
10
+ attr_accessor :class_ref
6
11
 
12
+ # (see Base#build)
7
13
  def build(builder)
8
14
  builder['samlp'].RequestedAuthnContext do |requested_authn_context|
9
15
  requested_authn_context.parent['Comparison'] = comparison.to_s if comparison
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'nokogiri-xmlsec'
2
4
  require 'time'
3
5
 
@@ -10,6 +12,12 @@ module SAML2
10
12
  class Response < StatusResponse
11
13
  attr_reader :assertions
12
14
 
15
+ # Respond to an {AuthnRequest}
16
+ # @param authn_request [AuthnRequest]
17
+ # @param issuer [NameID]
18
+ # @param name_id [NameID] The Subject
19
+ # @param attributes optional [Hash<String => String>, Array<Attribute>]
20
+ # @return [Response]
13
21
  def self.respond_to(authn_request, issuer, name_id, attributes = nil)
14
22
  response = initiate(nil, issuer, name_id)
15
23
  response.in_response_to = authn_request.id
@@ -26,6 +34,12 @@ module SAML2
26
34
  response
27
35
  end
28
36
 
37
+ # Begin an IdP Initiated login
38
+ # @param service_provider [ServiceProvider]
39
+ # @param issuer [NameID]
40
+ # @param name_id [NameID] The subject
41
+ # @param attributes optional [Hash<String => String>, Array<Attribute>]
42
+ # @return [Response]
29
43
  def self.initiate(service_provider, issuer, name_id, attributes = nil)
30
44
  response = new
31
45
  response.issuer = issuer
@@ -57,11 +71,13 @@ module SAML2
57
71
  @assertions = []
58
72
  end
59
73
 
74
+ # (see Base#from_xml)
60
75
  def from_xml(node)
61
76
  super
62
77
  remove_instance_variable(:@assertions)
63
78
  end
64
79
 
80
+ # @return [Array<Assertion>]
65
81
  def assertions
66
82
  unless instance_variable_defined?(:@assertions)
67
83
  @assertions = load_object_array(xml, 'saml:Assertion', Assertion)
@@ -69,8 +85,10 @@ module SAML2
69
85
  @assertions
70
86
  end
71
87
 
72
- def sign(*args)
73
- assertions.each { |assertion| assertion.sign(*args) }
88
+ # (see Signable#sign)
89
+ # Signs each assertion.
90
+ def sign(x509_certificate, private_key, algorithm_name = :sha256)
91
+ assertions.each { |assertion| assertion.sign(x509_certificate, private_key, algorithm_name) }
74
92
  # make sure we no longer pretty print this object
75
93
  @pretty = false
76
94
  nil
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'set'
2
4
 
3
5
  require 'saml2/base'
@@ -6,9 +8,10 @@ require 'saml2/organization_and_contacts'
6
8
  require 'saml2/signable'
7
9
 
8
10
  module SAML2
11
+ # @abstract
9
12
  class Role < Base
10
13
  module Protocols
11
- SAML2 = 'urn:oasis:names:tc:SAML:2.0:protocol'.freeze
14
+ SAML2 = 'urn:oasis:names:tc:SAML:2.0:protocol'
12
15
  end
13
16
 
14
17
  include OrganizationAndContacts
@@ -23,29 +26,36 @@ module SAML2
23
26
  @keys = []
24
27
  end
25
28
 
29
+ # (see Base#from_xml)
26
30
  def from_xml(node)
27
31
  super
28
32
  @supported_protocols = nil
29
33
  @keys = nil
30
34
  end
31
35
 
36
+ # @see Protocols
37
+ # @return [Array<String>]
32
38
  def supported_protocols
33
39
  @supported_protocols ||= xml['protocolSupportEnumeration'].split
34
40
  end
35
41
 
42
+ # @return [Array<KeyDescriptor>]
36
43
  def keys
37
- @keys ||= load_object_array(xml, 'md:KeyDescriptor', Key)
44
+ @keys ||= load_object_array(xml, 'md:KeyDescriptor', KeyDescriptor)
38
45
  end
39
46
 
47
+ # @return [Array<KeyDescriptor>]
40
48
  def signing_keys
41
49
  keys.select { |key| key.signing? }
42
50
  end
43
51
 
52
+ # @return [Array<KeyDescriptor>]
44
53
  def encryption_keys
45
54
  keys.select { |key| key.encryption? }
46
55
  end
47
56
 
48
57
  protected
58
+
49
59
  # should be called from inside the role element
50
60
  def build(builder)
51
61
  builder.parent['protocolSupportEnumeration'] = supported_protocols.to_a.join(' ')