saml2 1.0.10 → 1.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +2 -7
  3. data/lib/saml2.rb +2 -0
  4. data/lib/saml2/attribute.rb +2 -0
  5. data/lib/saml2/attribute_consuming_service.rb +1 -0
  6. data/lib/saml2/authn_request.rb +19 -47
  7. data/lib/saml2/base.rb +5 -2
  8. data/lib/saml2/bindings.rb +7 -0
  9. data/lib/saml2/bindings/http_redirect.rb +141 -0
  10. data/lib/saml2/contact.rb +14 -16
  11. data/lib/saml2/endpoint.rb +5 -6
  12. data/lib/saml2/entity.rb +23 -18
  13. data/lib/saml2/identity_provider.rb +4 -4
  14. data/lib/saml2/indexed_object.rb +7 -3
  15. data/lib/saml2/key.rb +19 -1
  16. data/lib/saml2/logout_request.rb +43 -0
  17. data/lib/saml2/logout_response.rb +23 -0
  18. data/lib/saml2/message.rb +109 -0
  19. data/lib/saml2/name_id.rb +16 -8
  20. data/lib/saml2/organization_and_contacts.rb +2 -2
  21. data/lib/saml2/request.rb +8 -0
  22. data/lib/saml2/response.rb +7 -23
  23. data/lib/saml2/role.rb +2 -3
  24. data/lib/saml2/service_provider.rb +24 -2
  25. data/lib/saml2/sso.rb +2 -2
  26. data/lib/saml2/status.rb +28 -0
  27. data/lib/saml2/status_response.rb +33 -0
  28. data/lib/saml2/version.rb +1 -1
  29. data/spec/fixtures/identity_provider.xml +1 -0
  30. data/spec/fixtures/response_signed.xml +1 -1
  31. data/spec/fixtures/response_with_attribute_signed.xml +1 -1
  32. data/spec/lib/attribute_consuming_service_spec.rb +37 -37
  33. data/spec/lib/attribute_spec.rb +17 -17
  34. data/spec/lib/authn_request_spec.rb +15 -71
  35. data/spec/lib/bindings/http_redirect_spec.rb +151 -0
  36. data/spec/lib/conditions_spec.rb +10 -10
  37. data/spec/lib/entity_spec.rb +12 -12
  38. data/spec/lib/identity_provider_spec.rb +4 -4
  39. data/spec/lib/indexed_object_spec.rb +38 -7
  40. data/spec/lib/logout_request_spec.rb +31 -0
  41. data/spec/lib/logout_response_spec.rb +31 -0
  42. data/spec/lib/message_spec.rb +21 -0
  43. data/spec/lib/response_spec.rb +8 -9
  44. data/spec/lib/service_provider_spec.rb +29 -8
  45. data/spec/spec_helper.rb +0 -1
  46. metadata +41 -11
@@ -23,21 +23,21 @@ module SAML2
23
23
 
24
24
  def want_authn_requests_signed?
25
25
  unless instance_variable_defined?(:@want_authn_requests_signed)
26
- @want_authn_requests_signed = @root['WantAuthnRequestsSigned'] && @root['WantAuthnRequestsSigned'] == 'true'
26
+ @want_authn_requests_signed = xml['WantAuthnRequestsSigned'] && xml['WantAuthnRequestsSigned'] == 'true'
27
27
  end
28
28
  @want_authn_requests_signed
29
29
  end
30
30
 
31
31
  def single_sign_on_services
32
- @single_sign_on_services ||= load_object_array(@root, 'md:SingleSignOnService', Endpoint)
32
+ @single_sign_on_services ||= load_object_array(xml, 'md:SingleSignOnService', Endpoint)
33
33
  end
34
34
 
35
35
  def attribute_profiles
36
- @attribute_profiles ||= load_string_array(@root, 'md:AttributeProfile')
36
+ @attribute_profiles ||= load_string_array(xml, 'md:AttributeProfile')
37
37
  end
38
38
 
39
39
  def attributes
40
- @attributes ||= load_object_array(@root, 'saml:Attribute', Attribute)
40
+ @attributes ||= load_object_array(xml, 'saml:Attribute', Attribute)
41
41
  end
42
42
 
43
43
  def build(builder)
@@ -19,6 +19,10 @@ module SAML2
19
19
  !!@is_default
20
20
  end
21
21
 
22
+ def default_defined?
23
+ !@is_default.nil?
24
+ end
25
+
22
26
  def from_xml(node)
23
27
  @index = node['index'] && node['index'].to_i
24
28
  @is_default = node['isDefault'] && node['isDefault'] == 'true'
@@ -50,10 +54,10 @@ module SAML2
50
54
  end
51
55
  end
52
56
 
53
- def build(builder)
57
+ def build(builder, *)
54
58
  super
55
- builder.parent.last['index'] = index
56
- builder.parent.last['isDefault'] = default? unless default?.nil?
59
+ builder.parent.children.last['index'] = index
60
+ builder.parent.children.last['isDefault'] = default? if default_defined?
57
61
  end
58
62
 
59
63
  private
@@ -7,6 +7,24 @@ module SAML2
7
7
  SIGNING = 'signing'.freeze
8
8
  end
9
9
 
10
+ class EncryptionMethod
11
+ module Algorithm
12
+ AES128_CBC = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'.freeze
13
+ end
14
+
15
+ attr_accessor :algorithm, :key_size
16
+
17
+ def initialize(algorithm = Algorithm::AES128_CBC, key_size = 128)
18
+ @algorithm, @key_size = algorithm, key_size
19
+ end
20
+
21
+ def build(builder)
22
+ builder['md'].EncryptionMethod('Algorithm' => algorithm) do |encryption_method|
23
+ encryption_method['xenc'].KeySize(key_size) if key_size
24
+ end
25
+ end
26
+ end
27
+
10
28
  attr_accessor :use, :x509, :encryption_methods
11
29
 
12
30
  def self.from_xml(node)
@@ -46,7 +64,7 @@ module SAML2
46
64
  end
47
65
  end
48
66
  encryption_methods.each do |method|
49
- key_descriptor['xenc'].EncryptionMethod('Algorithm' => method)
67
+ method.build(key_descriptor)
50
68
  end
51
69
  end
52
70
  end
@@ -0,0 +1,43 @@
1
+ require 'saml2/name_id'
2
+ require 'saml2/request'
3
+
4
+ module SAML2
5
+ class LogoutRequest < Request
6
+ attr_accessor :name_id, :session_index
7
+
8
+ def self.initiate(sso, issuer, name_id, session_index = nil)
9
+ logout_request = new
10
+ logout_request.issuer = issuer
11
+ logout_request.destination = sso.single_logout_services.first.location
12
+ logout_request.name_id = name_id
13
+ logout_request.session_index = session_index
14
+
15
+ logout_request
16
+ end
17
+
18
+ def name_id
19
+ @name_id ||= (NameID.from_xml(xml.at_xpath('saml:NameID', Namespaces::ALL)) if xml)
20
+ end
21
+
22
+ def session_index
23
+ @session_index ||= (load_string_array(xml,'samlp:SessionIndex') if xml)
24
+ end
25
+
26
+ private
27
+
28
+ def build(builder)
29
+ builder['samlp'].LogoutRequest(
30
+ 'xmlns:samlp' => Namespaces::SAMLP,
31
+ 'xmlns:saml' => Namespaces::SAML
32
+ ) do |logout_request|
33
+ super(logout_request)
34
+
35
+ name_id.build(logout_request)
36
+
37
+ Array(session_index).each do |session_index_instance|
38
+ logout_request['samlp'].SessionIndex(session_index_instance)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,23 @@
1
+ require 'saml2/status_response'
2
+
3
+ module SAML2
4
+ class LogoutResponse < StatusResponse
5
+ def self.respond_to(logout_request, sso, issuer, status_code = Status::SUCCESS)
6
+ logout_response = new
7
+ logout_response.issuer = issuer
8
+ logout_response.destination = sso.single_logout_services.first.location
9
+ logout_response.in_response_to = logout_request.id
10
+ logout_response.status.code = status_code
11
+ logout_response
12
+ end
13
+
14
+ def build(builder)
15
+ builder['samlp'].LogoutResponse(
16
+ 'xmlns:samlp' => Namespaces::SAMLP,
17
+ 'xmlns:saml' => Namespaces::SAML
18
+ ) do |logout_response|
19
+ super(logout_response)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,109 @@
1
+ require 'securerandom'
2
+ require 'time'
3
+
4
+ require 'saml2/base'
5
+
6
+ module SAML2
7
+ class InvalidMessage < RuntimeError
8
+ end
9
+
10
+ class MissingMessage < InvalidMessage
11
+ end
12
+
13
+ class CorruptMessage < InvalidMessage
14
+ end
15
+
16
+ class MessageTooLarge < InvalidMessage
17
+ end
18
+
19
+ class UnknownMessage < InvalidMessage
20
+ end
21
+
22
+ class UnexpectedMessage < InvalidMessage
23
+ end
24
+
25
+ class UnsupportedEncoding < InvalidMessage
26
+ end
27
+
28
+ class UnsupportedSignatureAlgorithm < InvalidMessage
29
+ end
30
+
31
+ class InvalidSignature < InvalidMessage
32
+ end
33
+
34
+ class UnsignedMessage < InvalidMessage
35
+ end
36
+
37
+ # In the SAML Schema, Request and Response don't technically share a common
38
+ # ancestor, but they have several things in common so it's useful to represent
39
+ # that here
40
+ class Message < Base
41
+ attr_reader :id, :issue_instant
42
+ attr_accessor :issuer, :destination
43
+
44
+ class << self
45
+ def inherited(klass)
46
+ # explicitly keep track of all messages in this base class
47
+ Message.known_messages[klass.name.sub(/^SAML2::/, '')] = klass
48
+ end
49
+
50
+ def from_xml(node)
51
+ return super unless self == Message
52
+ klass = Message.known_messages[node.name]
53
+ raise UnknownMessage.new("Unknown message #{node.name}") unless klass
54
+ klass.from_xml(node)
55
+ end
56
+
57
+ def parse(xml)
58
+ result = Message.from_xml(Nokogiri::XML(xml) { |config| config.strict }.root)
59
+ raise UnexpectedMessage.new("Expected a #{self.name}, but got a #{result.class.name}") unless self == Message || result.class == self
60
+ result
61
+ rescue Nokogiri::XML::SyntaxError
62
+ raise CorruptMessage
63
+ end
64
+
65
+ protected
66
+
67
+ def known_messages
68
+ @known_messages ||= {}
69
+ end
70
+ end
71
+
72
+ def initialize
73
+ @id = "_#{SecureRandom.uuid}"
74
+ @issue_instant = Time.now.utc
75
+ end
76
+
77
+ def from_xml(node)
78
+ super
79
+ @id = nil
80
+ @issue_instant = nil
81
+ end
82
+
83
+ def valid_schema?
84
+ return false unless Schemas.protocol.valid?(xml.document)
85
+
86
+ true
87
+ end
88
+
89
+ def issuer
90
+ @issuer ||= NameID.from_xml(xml.at_xpath('saml:Issuer', Namespaces::ALL))
91
+ end
92
+
93
+ def id
94
+ @id ||= xml['ID']
95
+ end
96
+
97
+ protected
98
+
99
+ # should be called from inside the specific request element
100
+ def build(message)
101
+ message.parent['ID'] = id
102
+ message.parent['Version'] = '2.0'
103
+ message.parent['IssueInstant'] = issue_instant.iso8601
104
+ message.parent['Destination'] = destination if destination
105
+
106
+ issuer.build(message, element: 'Issuer') if issuer
107
+ end
108
+ end
109
+ end
@@ -36,25 +36,33 @@ module SAML2
36
36
  end
37
37
  end
38
38
 
39
- attr_reader :id, :format
39
+ attr_accessor :id, :format, :name_qualifier, :sp_name_qualifier
40
40
 
41
41
  def self.from_xml(node)
42
- node && new(node.content.strip, node['Format'])
42
+ node && new(node.content.strip,
43
+ node['Format'],
44
+ name_qualifier: node['NameQualifier'],
45
+ sp_name_qualifier: node['SPNameQualifier'])
43
46
  end
44
47
 
45
- def initialize(id = nil, format = nil)
46
- @id, @format = id, format
48
+ def initialize(id = nil, format = nil, name_qualifier: nil, sp_name_qualifier: nil)
49
+ @id, @format, @name_qualifier, @sp_name_qualifier =
50
+ id, format, name_qualifier, sp_name_qualifier
47
51
  end
48
52
 
49
53
  def ==(rhs)
50
- id == rhs.id && format == rhs.format
54
+ id == rhs.id &&
55
+ format == rhs.format &&
56
+ name_qualifier == rhs.name_qualifier &&
57
+ sp_name_qualifier == rhs.sp_name_qualifier
51
58
  end
52
59
 
53
- def build(builder, options = {})
60
+ def build(builder, element: nil)
54
61
  args = {}
55
62
  args['Format'] = format if format
56
- args['xmlns:saml'] = Namespaces::SAML if options[:include_namespace]
57
- builder['saml'].__send__(options.delete(:element) || 'NameID', id, args)
63
+ args['NameQualifier'] = name_qualifier if name_qualifier
64
+ args['SPNameQualifier'] = sp_name_qualifier if sp_name_qualifier
65
+ builder['saml'].__send__(element || 'NameID', id, args)
58
66
  end
59
67
  end
60
68
  end
@@ -18,13 +18,13 @@ module SAML2
18
18
 
19
19
  def organization
20
20
  unless instance_variable_defined?(:@organization)
21
- @organization = Organization.from_xml(@root.at_xpath('md:Organization', Namespaces::ALL))
21
+ @organization = Organization.from_xml(xml.at_xpath('md:Organization', Namespaces::ALL))
22
22
  end
23
23
  @organization
24
24
  end
25
25
 
26
26
  def contacts
27
- @contacts ||= load_object_array(@root, 'md:ContactPerson', Contact)
27
+ @contacts ||= load_object_array(xml, 'md:ContactPerson', Contact)
28
28
  end
29
29
 
30
30
  protected
@@ -0,0 +1,8 @@
1
+ require 'saml2/message'
2
+ require 'saml2/name_id'
3
+ require 'saml2/namespaces'
4
+
5
+ module SAML2
6
+ class Request < Message
7
+ end
8
+ end
@@ -1,20 +1,14 @@
1
1
  require 'nokogiri-xmlsec'
2
- require 'securerandom'
3
2
  require 'time'
4
3
 
5
4
  require 'saml2/assertion'
6
5
  require 'saml2/authn_statement'
7
- require 'saml2/base'
6
+ require 'saml2/status_response'
8
7
  require 'saml2/subject'
9
8
 
10
9
  module SAML2
11
- class Response < Base
12
- module Status
13
- SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success".freeze
14
- end
15
-
16
- attr_reader :id, :issue_instant, :assertions
17
- attr_accessor :issuer, :in_response_to, :destination, :status_code
10
+ class Response < StatusResponse
11
+ attr_reader :assertions
18
12
 
19
13
  def self.respond_to(authn_request, issuer, name_id, attributes = nil)
20
14
  response = initiate(nil, issuer, name_id)
@@ -59,9 +53,7 @@ module SAML2
59
53
  end
60
54
 
61
55
  def initialize
62
- @id = "_#{SecureRandom.uuid}"
63
- @status_code = Status::SUCCESS
64
- @issue_instant = Time.now.utc
56
+ super
65
57
  @assertions = []
66
58
  end
67
59
 
@@ -70,21 +62,13 @@ module SAML2
70
62
  end
71
63
 
72
64
  private
65
+
73
66
  def build(builder)
74
67
  builder['samlp'].Response(
75
68
  'xmlns:samlp' => Namespaces::SAMLP,
76
- ID: id,
77
- Version: '2.0',
78
- IssueInstant: issue_instant.iso8601,
79
- Destination: destination
69
+ 'xmlns:saml' => Namespaces::SAML
80
70
  ) do |response|
81
- response.parent['InResponseTo'] = in_response_to if in_response_to
82
-
83
- issuer.build(response, element: 'Issuer', include_namespace: true) if issuer
84
-
85
- response['samlp'].Status do |status|
86
- status['samlp'].StatusCode(Value: status_code)
87
- end
71
+ super(response)
88
72
 
89
73
  assertions.each do |assertion|
90
74
  response.parent << assertion.to_xml
@@ -23,17 +23,16 @@ module SAML2
23
23
 
24
24
  def from_xml(node)
25
25
  super
26
- @root = node
27
26
  @supported_protocols = nil
28
27
  @keys = nil
29
28
  end
30
29
 
31
30
  def supported_protocols
32
- @supported_protocols ||= @root['protocolSupportEnumeration'].split
31
+ @supported_protocols ||= xml['protocolSupportEnumeration'].split
33
32
  end
34
33
 
35
34
  def keys
36
- @keys ||= load_object_array(@root, 'md:KeyDescriptor', Key)
35
+ @keys ||= load_object_array(xml, 'md:KeyDescriptor', Key)
37
36
  end
38
37
 
39
38
  def signing_keys
@@ -5,18 +5,40 @@ require 'saml2/sso'
5
5
 
6
6
  module SAML2
7
7
  class ServiceProvider < SSO
8
+ def initialize
9
+ super
10
+ @assertion_consumer_services = []
11
+ @attribute_consuming_services = []
12
+ end
13
+
14
+ def from_xml(node)
15
+ super
16
+ @assertion_consumer_services = nil
17
+ @attribute_consuming_services = nil
18
+ end
19
+
8
20
  def assertion_consumer_services
9
21
  @assertion_consumer_services ||= begin
10
- nodes = @root.xpath('md:AssertionConsumerService', Namespaces::ALL)
22
+ nodes = xml.xpath('md:AssertionConsumerService', Namespaces::ALL)
11
23
  Endpoint::Indexed::Array.from_xml(nodes)
12
24
  end
13
25
  end
14
26
 
15
27
  def attribute_consuming_services
16
28
  @attribute_consuming_services ||= begin
17
- nodes = @root.xpath('md:AttributeConsumingService', Namespaces::ALL)
29
+ nodes = xml.xpath('md:AttributeConsumingService', Namespaces::ALL)
18
30
  AttributeConsumingService::Array.from_xml(nodes)
19
31
  end
20
32
  end
33
+
34
+ def build(builder)
35
+ builder['md'].SPSSODescriptor do |sp_sso_descriptor|
36
+ super(sp_sso_descriptor)
37
+
38
+ assertion_consumer_services.each do |acs|
39
+ acs.build(sp_sso_descriptor, 'AssertionConsumerService')
40
+ end
41
+ end
42
+ end
21
43
  end
22
44
  end