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,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(' ')