saml2 1.0.10 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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