saml2 1.1.5 → 2.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3fd767fa1f70be5220dc97ce1c23908fa77f9956
4
- data.tar.gz: 38629f1bb5913371f916a324db13492d5fe482cb
3
+ metadata.gz: 49552e3c1623dc97bebb250e582f14925c243a01
4
+ data.tar.gz: ec92142eee88b9f0bb7b53cb747ca7c9c874ea52
5
5
  SHA512:
6
- metadata.gz: 52aafbc49cee69b4a56aea2cbc2b287b9129baafd9ed11fddf605bdde16d9a732e37e995c76ac0fec5d0a1f273c9a6436b2e90043a88ae79d999eeb0bf65653b
7
- data.tar.gz: b917c7be4728202fdfc2021628ee3feea6decc38c203eaba8e1152bf2e5920611729cfcf1d8e87c2077ab76a74ccb29e9aa9bc48cf15ece816065ba134c527e8
6
+ metadata.gz: 22a08e29c00544c48402463bf5615829348abffc0ca79b823f8d594480a422af4ecef8e49cd934a6898d16c5f6dcb890b3183c6fc8d62d0796f21532d87a6778
7
+ data.tar.gz: a000f59983fff2fc9c2b6385d5a2f94076f15fee45503c8b58de2c2647b112fa954503c3023ca2baf5c049e493f023e386524b8646ff7b254090eb2119f7503c
@@ -1,45 +1,48 @@
1
1
  require 'saml2/conditions'
2
2
 
3
3
  module SAML2
4
- class Assertion
5
- attr_reader :id, :issue_instant, :conditions, :statements
6
- attr_accessor :issuer, :subject
4
+ class Assertion < Message
5
+ attr_writer :statements, :subject
7
6
 
8
7
  def initialize
9
- @id = "_#{SecureRandom.uuid}"
10
- @issue_instant = Time.now.utc
8
+ super
11
9
  @statements = []
12
10
  @conditions = Conditions.new
13
11
  end
14
12
 
15
- def sign(x509_certificate, private_key, algorithm_name = :sha256)
16
- to_xml
13
+ def from_xml(node)
14
+ super
15
+ @conditions = nil
16
+ @statements = nil
17
+ end
18
+
19
+ def subject
20
+ if xml && !instance_variable_defined?(:@subject)
21
+ @subject = Subject.from_xml(xml.at_xpath('saml:Subject', Namespaces::ALL))
22
+ end
23
+ @subject
24
+ end
25
+
26
+ def conditions
27
+ @conditions ||= Conditions.from_xml(xml.at_xpath('saml:Conditions', Namespaces::ALL))
28
+ end
17
29
 
18
- @xml.set_id_attribute('ID')
19
- @xml.sign!(cert: x509_certificate, key: private_key, digest_alg: algorithm_name.to_s, signature_alg: "rsa-#{algorithm_name}", uri: "##{id}")
20
- # the Signature element must be right after the Issuer, so put it there
21
- issuer = @xml.at_xpath("saml:Issuer", Namespaces::ALL)
22
- signature = @xml.at_xpath("dsig:Signature", Namespaces::ALL)
23
- issuer.add_next_sibling(signature)
24
- self
30
+ def statements
31
+ @statements ||= load_object_array(xml, 'saml:AuthnStatement|saml:AttributeStatement')
25
32
  end
26
33
 
27
- def to_xml
28
- @xml ||= Nokogiri::XML::Builder.new do |builder|
29
- builder['saml'].Assertion(
30
- 'xmlns:saml' => Namespaces::SAML,
31
- ID: id,
32
- Version: '2.0',
33
- IssueInstant: issue_instant.iso8601
34
- ) do |assertion|
35
- issuer.build(assertion, element: 'Issuer')
36
-
37
- subject.build(assertion)
38
-
39
- conditions.build(assertion)
40
- statements.each { |stmt| stmt.build(assertion) }
41
- end
42
- end.doc.root
34
+ def build(builder)
35
+ builder['saml'].Assertion(
36
+ 'xmlns:saml' => Namespaces::SAML
37
+ ) do |assertion|
38
+ super(assertion)
39
+
40
+ subject.build(assertion)
41
+
42
+ conditions.build(assertion)
43
+
44
+ statements.each { |stmt| stmt.build(assertion) }
45
+ end
43
46
  end
44
47
  end
45
48
  end
@@ -32,10 +32,17 @@ module SAML2
32
32
  end
33
33
 
34
34
  def create(name, value = nil)
35
-
36
35
  (class_for(name) || self).new(name, value)
37
36
  end
38
37
 
38
+ def namespace
39
+ 'saml'
40
+ end
41
+
42
+ def element
43
+ 'Attribute'
44
+ end
45
+
39
46
  protected
40
47
 
41
48
  def class_for(name_or_node)
@@ -52,7 +59,7 @@ module SAML2
52
59
  end
53
60
 
54
61
  def build(builder)
55
- builder['saml'].Attribute('Name' => name) do |attribute|
62
+ builder[self.class.namespace].__send__(self.class.element, 'Name' => name) do |attribute|
56
63
  attribute.parent['FriendlyName'] = friendly_name if friendly_name
57
64
  attribute.parent['NameFormat'] = name_format if name_format
58
65
  Array.wrap(value).each do |value|
@@ -2,12 +2,30 @@ require 'active_support/core_ext/array/wrap'
2
2
 
3
3
  require 'saml2/attribute'
4
4
  require 'saml2/indexed_object'
5
+ require 'saml2/localized_name'
5
6
  require 'saml2/namespaces'
6
7
 
7
8
  module SAML2
8
9
  class RequestedAttribute < Attribute
9
- def initialize(name = nil, is_required = nil, name_format = nil)
10
- super(name, nil, nil, name_format)
10
+ class << self
11
+ def namespace
12
+ 'md'
13
+ end
14
+
15
+ def element
16
+ 'RequestedAttribute'
17
+ end
18
+
19
+ def create(name, is_required = nil)
20
+ # use Attribute.create to get other subclasses to automatically fill out friendly_name
21
+ # and name_format, but still return a RequestedAttribute object
22
+ attribute = Attribute.create(name)
23
+ new(attribute.name, is_required, attribute.friendly_name, attribute.name_format)
24
+ end
25
+ end
26
+
27
+ def initialize(name = nil, is_required = nil, friendly_name = nil, name_format = nil)
28
+ super(name, nil, friendly_name, name_format)
11
29
  @is_required = is_required
12
30
  end
13
31
 
@@ -44,16 +62,19 @@ module SAML2
44
62
  class AttributeConsumingService < Base
45
63
  include IndexedObject
46
64
 
47
- attr_reader :name, :requested_attributes
65
+ attr_reader :name, :description, :requested_attributes
48
66
 
49
67
  def initialize(name = nil, requested_attributes = [])
50
68
  super()
51
- @name, @requested_attributes = name, requested_attributes
69
+ @name = LocalizedName.new('ServiceName', name)
70
+ @description = LocalizedName.new('ServiceDescription')
71
+ @requested_attributes = requested_attributes
52
72
  end
53
73
 
54
74
  def from_xml(node)
55
75
  super
56
- @name = node['ServiceName']
76
+ name.from_xml(node.xpath('md:ServiceName', Namespaces::ALL))
77
+ description.from_xml(node.xpath('md:ServiceDescription', Namespaces::ALL))
57
78
  @requested_attributes = load_object_array(node, "md:RequestedAttribute", RequestedAttribute)
58
79
  end
59
80
 
@@ -97,5 +118,16 @@ module SAML2
97
118
  return nil if attributes.empty?
98
119
  AttributeStatement.new(attributes)
99
120
  end
121
+
122
+ def build(builder)
123
+ builder['md'].AttributeConsumingService do |attribute_consuming_service|
124
+ name.build(attribute_consuming_service)
125
+ description.build(attribute_consuming_service)
126
+ requested_attributes.each do |requested_attribute|
127
+ requested_attribute.build(attribute_consuming_service)
128
+ end
129
+ end
130
+ super
131
+ end
100
132
  end
101
133
  end
@@ -13,15 +13,6 @@ require 'saml2/subject'
13
13
 
14
14
  module SAML2
15
15
  class AuthnRequest < Request
16
- # deprecated; takes _just_ the SAMLRequest parameter's value
17
- def self.decode(authnrequest)
18
- result, _relay_state = Bindings::HTTPRedirect.decode("http://host/?SAMLRequest=#{CGI.escape(authnrequest)}")
19
- return nil unless result.is_a?(AuthnRequest)
20
- result
21
- rescue CorruptMessage
22
- AuthnRequest.from_xml(Nokogiri::XML('<xml></xml>').root)
23
- end
24
-
25
16
  attr_writer :assertion_consumer_service_index,
26
17
  :assertion_consumer_service_url,
27
18
  :attribute_consuming_service_index,
@@ -1,5 +1,7 @@
1
+ require 'saml2/base'
2
+
1
3
  module SAML2
2
- class AuthnStatement
4
+ class AuthnStatement < Base
3
5
  module Classes
4
6
  INTERNET_PROTOCOL = "urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocol".freeze # IP address
5
7
  INTERNET_PROTOCOL_PASSWORD = "urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword".freeze # IP address, as well as username/password
@@ -13,10 +15,20 @@ module SAML2
13
15
  UNSPECIFIED = "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified".freeze
14
16
  end
15
17
 
16
- attr_accessor :authn_instant, :authn_context_class_ref
18
+ attr_accessor :authn_instant, :authn_context_class_ref, :session_index, :session_not_on_or_after
19
+
20
+ def from_xml(node)
21
+ super
22
+ @authn_instant = Time.parse(node['AuthnInstant'])
23
+ @session_index = node['SessionIndex']
24
+ @session_not_on_or_after = Time.parse(node['SessionNotOnOrAfter']) if node['SessionNotOnOrAfter']
25
+ @authn_context_class_ref = node.at_xpath('saml:AuthnContext/saml:AuthnContextClassRef', Namespaces::ALL)&.content&.strip
26
+ end
17
27
 
18
28
  def build(builder)
19
29
  builder['saml'].AuthnStatement('AuthnInstant' => authn_instant.iso8601) do |authn_statement|
30
+ authn_statement.parent['SessionIndex'] = session_index if session_index
31
+ authn_statement.parent['SessionNotOnOrAfter'] = session_not_on_or_after.iso8601 if session_not_on_or_after
20
32
  authn_statement['saml'].AuthnContext do |authn_context|
21
33
  authn_context['saml'].AuthnContextClassRef(authn_context_class_ref) if authn_context_class_ref
22
34
  end
data/lib/saml2/base.rb CHANGED
@@ -15,9 +15,19 @@ module SAML2
15
15
  @xml = node
16
16
  end
17
17
 
18
- def to_s
19
- # make sure to not FORMAT it - it breaks signatures!
20
- to_xml.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML | Nokogiri::XML::Node::SaveOptions::NO_DECLARATION)
18
+ def to_s(pretty: true)
19
+ if xml
20
+ xml.to_s
21
+ elsif pretty
22
+ to_xml.to_s
23
+ else
24
+ # make sure to not FORMAT it - it breaks signatures!
25
+ to_xml.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML | Nokogiri::XML::Node::SaveOptions::NO_DECLARATION)
26
+ end
27
+ end
28
+
29
+ def inspect
30
+ "#<#{self.class.name} #{instance_variables.map { |iv| next if iv == :@xml; "#{iv}=#{instance_variable_get(iv).inspect}" }.compact.join(", ") }>"
21
31
  end
22
32
 
23
33
  def to_xml
@@ -25,19 +35,27 @@ module SAML2
25
35
  builder = Nokogiri::XML::Builder.new
26
36
  build(builder)
27
37
  @document = builder.doc
38
+ # if we're re-serializing a parsed document (i.e. after mutating/parsing it),
39
+ # forget the original document we parsed
40
+ @xml = nil
28
41
  end
29
42
  @document
30
43
  end
31
44
 
45
+ def build(builder)
46
+ end
47
+
32
48
  def self.load_string_array(node, element)
33
49
  node.xpath(element, Namespaces::ALL).map do |element_node|
34
50
  element_node.content&.strip
35
51
  end
36
52
  end
37
53
 
38
- def self.load_object_array(node, element, klass)
54
+ def self.load_object_array(node, element, klass = nil)
39
55
  node.xpath(element, Namespaces::ALL).map do |element_node|
40
- if klass.is_a?(Hash)
56
+ if klass.nil?
57
+ SAML2.const_get(element_node.name, false).from_xml(element_node)
58
+ elsif klass.is_a?(Hash)
41
59
  klass[element_node.name].from_xml(element_node)
42
60
  else
43
61
  klass.from_xml(element_node)
@@ -51,11 +69,12 @@ module SAML2
51
69
  end
52
70
 
53
71
  protected
72
+
54
73
  def load_string_array(node, element)
55
74
  self.class.load_string_array(node, element)
56
75
  end
57
76
 
58
- def load_object_array(node, element, klass)
77
+ def load_object_array(node, element, klass = nil)
59
78
  self.class.load_object_array(node, element, klass)
60
79
  end
61
80
 
@@ -1,7 +1,35 @@
1
+ require 'base64'
2
+
1
3
  module SAML2
2
4
  module Bindings
3
5
  module HTTP_POST
4
6
  URN ="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST".freeze
7
+
8
+ class << self
9
+ def decode(post_params)
10
+ base64 = post_params['SAMLRequest'] || post_params['SAMLResponse']
11
+ raise MissingMessage unless base64
12
+
13
+ raise MessageTooLarge if base64.bytesize > SAML2.config[:max_message_size]
14
+
15
+ xml = begin
16
+ Base64.decode64(base64)
17
+ rescue ArgumentError
18
+ raise CorruptMessage
19
+ end
20
+
21
+ message = Message.parse(xml)
22
+ [message, post_params['RelayState']]
23
+ end
24
+
25
+ def encode(message, relay_state: nil)
26
+ xml = message.to_s(pretty: false)
27
+ key = message.is_a?(Request) ? 'SAMLRequest' : 'SAMLResponse'
28
+ post_params = { key => Base64.encode64(xml) }
29
+ post_params['RelayState'] = relay_state if relay_state
30
+ post_params
31
+ end
32
+ end
5
33
  end
6
34
  end
7
35
  end
@@ -107,7 +107,7 @@ module SAML2
107
107
  original_query.delete_if { |(k, v)| k == param }
108
108
  end
109
109
 
110
- xml = message.to_s
110
+ xml = message.to_s(pretty: false)
111
111
  zstream = Zlib::Deflate.new(Zlib::BEST_COMPRESSION, -Zlib::MAX_WBITS)
112
112
  deflated = zstream.deflate(xml, Zlib::FINISH)
113
113
  zstream.close
@@ -3,6 +3,21 @@ require 'active_support/core_ext/array/wrap'
3
3
  module SAML2
4
4
  class Conditions < Array
5
5
  attr_accessor :not_before, :not_on_or_after
6
+ attr_reader :xml
7
+
8
+ def self.from_xml(node)
9
+ result = new
10
+ result.from_xml(node)
11
+ result
12
+ end
13
+
14
+ def from_xml(node)
15
+ @xml = node
16
+ @not_before = Time.parse(node['NotBefore']) if node['NotBefore']
17
+ @not_on_or_after = Time.parse(node['NotOnOrAfter']) if node['NotOnOrAfter']
18
+
19
+ replace(node.children.map { |restriction| self.class.const_get(restriction.name, false).from_xml(restriction) })
20
+ end
6
21
 
7
22
  def valid?(options = {})
8
23
  now = options[:now] || Time.now
@@ -37,19 +52,28 @@ module SAML2
37
52
  end
38
53
 
39
54
  # Any unknown condition
40
- class Condition
55
+ class Condition < Base
41
56
  def valid?(_)
42
57
  :indeterminate
43
58
  end
44
59
  end
45
60
 
46
61
  class AudienceRestriction < Condition
47
- attr_accessor :audience
62
+ attr_writer :audience
48
63
 
49
- def initialize(audience)
64
+ def initialize(audience = [])
50
65
  @audience = audience
51
66
  end
52
67
 
68
+ def from_xml(node)
69
+ super
70
+ @audience = nil
71
+ end
72
+
73
+ def audience
74
+ @audience ||= load_string_array(xml, 'saml:Audience')
75
+ end
76
+
53
77
  def valid?(options)
54
78
  Array.wrap(audience).include?(options[:audience]) ? :valid : :invalid
55
79
  end
@@ -1,16 +1,10 @@
1
- require 'saml2/bindings/http_redirect'
2
1
  require 'saml2/bindings/http_post'
3
2
 
4
3
  module SAML2
5
4
  class Endpoint < Base
6
- module Bindings
7
- HTTP_POST = ::SAML2::Bindings::HTTP_POST::URN
8
- HTTP_REDIRECT = ::SAML2::Bindings::HTTPRedirect::URN
9
- end
10
-
11
5
  attr_reader :location, :binding
12
6
 
13
- def initialize(location = nil, binding = ::SAML2::Bindings::HTTP_POST::URN)
7
+ def initialize(location = nil, binding = Bindings::HTTP_POST::URN)
14
8
  @location, @binding = location, binding
15
9
  end
16
10
 
@@ -31,7 +25,7 @@ module SAML2
31
25
  class Indexed < Endpoint
32
26
  include IndexedObject
33
27
 
34
- def initialize(location = nil, index = nil, is_default = nil, binding = ::SAML2::Bindings::HTTP_POST::URN)
28
+ def initialize(location = nil, index = nil, is_default = nil, binding = Bindings::HTTP_POST::URN)
35
29
  super(location, binding)
36
30
  @index, @is_default = index, is_default
37
31
  end
data/lib/saml2/entity.rb CHANGED
@@ -4,10 +4,12 @@ require 'saml2/base'
4
4
  require 'saml2/identity_provider'
5
5
  require 'saml2/organization_and_contacts'
6
6
  require 'saml2/service_provider'
7
+ require 'saml2/signable'
7
8
 
8
9
  module SAML2
9
10
  class Entity < Base
10
11
  include OrganizationAndContacts
12
+ include Signable
11
13
 
12
14
  attr_writer :entity_id
13
15
 
@@ -28,6 +30,8 @@ module SAML2
28
30
 
29
31
  class Group < Base
30
32
  include Enumerable
33
+ include Signable
34
+
31
35
  [:each, :[]].each do |method|
32
36
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
33
37
  def #{method}(*args, &block)
@@ -38,11 +42,13 @@ module SAML2
38
42
 
39
43
  def initialize
40
44
  @entities = []
45
+ @id = "_#{SecureRandom.uuid}"
41
46
  @valid_until = nil
42
47
  end
43
48
 
44
49
  def from_xml(node)
45
50
  super
51
+ @id = nil
46
52
  remove_instance_variable(:@valid_until)
47
53
  @entities = Base.load_object_array(xml, "md:EntityDescriptor|md:EntitiesDescriptor",
48
54
  'EntityDescriptor' => Entity,
@@ -53,23 +59,8 @@ module SAML2
53
59
  Schemas.federation.valid?(xml.document)
54
60
  end
55
61
 
56
- def signature
57
- unless instance_variable_defined?(:@signature)
58
- @signature = xml.at_xpath('dsig:Signature', Namespaces::ALL)
59
- signed_node = @signature.at_xpath('dsig:SignedInfo/dsig:Reference', Namespaces::ALL)['URI']
60
- # validating the schema will automatically add ID attributes, so check that first
61
- xml.set_id_attribute('ID') unless xml.document.get_id(xml['ID'])
62
- @signature = nil unless signed_node == "##{xml['ID']}"
63
- end
64
- @signature
65
- end
66
-
67
- def signed?
68
- !!signature
69
- end
70
-
71
- def valid_signature?(*args)
72
- signature.verify_with(*args)
62
+ def id
63
+ @id ||= xml['ID']
73
64
  end
74
65
 
75
66
  def valid_until
@@ -85,10 +76,12 @@ module SAML2
85
76
  @valid_until = nil
86
77
  @entity_id = nil
87
78
  @roles = []
79
+ @id = "_#{SecureRandom.uuid}"
88
80
  end
89
81
 
90
82
  def from_xml(node)
91
83
  super
84
+ @id = nil
92
85
  remove_instance_variable(:@valid_until)
93
86
  @roles = nil
94
87
  end
@@ -101,6 +94,10 @@ module SAML2
101
94
  @entity_id || xml && xml['entityID']
102
95
  end
103
96
 
97
+ def id
98
+ @id ||= xml['ID']
99
+ end
100
+
104
101
  def valid_until
105
102
  unless instance_variable_defined?(:@valid_until)
106
103
  @valid_until = xml['validUntil'] && Time.parse(xml['validUntil'])
@@ -126,6 +123,8 @@ module SAML2
126
123
  'xmlns:md' => Namespaces::METADATA,
127
124
  'xmlns:dsig' => Namespaces::DSIG,
128
125
  'xmlns:xenc' => Namespaces::XENC) do |entity_descriptor|
126
+ entity_descriptor.parent['ID'] = id if id
127
+
129
128
  roles.each do |role|
130
129
  role.build(entity_descriptor)
131
130
  end
@@ -2,7 +2,7 @@ require 'saml2/base'
2
2
 
3
3
  module SAML2
4
4
  module IndexedObject
5
- attr_reader :index
5
+ attr_accessor :index
6
6
 
7
7
  def initialize(*args)
8
8
  @is_default = nil
@@ -57,8 +57,13 @@ module SAML2
57
57
  protected
58
58
 
59
59
  def re_index
60
+ last_index = -1
60
61
  @index = {}
61
- each { |object| @index[object.index] = object }
62
+ each do |object|
63
+ object.index ||= last_index + 1
64
+ last_index = object.index
65
+ @index[object.index] = object
66
+ end
62
67
  @default = find { |object| object.default? } || first
63
68
  end
64
69
  end
data/lib/saml2/key.rb CHANGED
@@ -51,8 +51,12 @@ module SAML2
51
51
  @certificate ||= OpenSSL::X509::Certificate.new(Base64.decode64(x509))
52
52
  end
53
53
 
54
+ def self.format_fingerprint(fingerprint)
55
+ fingerprint.downcase.gsub(/(\h{2})(?=\h)/, '\1:')
56
+ end
57
+
54
58
  def fingerprint
55
- @fingerprint ||= Digest::SHA1.hexdigest(certificate.to_der).gsub(/(\h{2})(?=\h)/, '\1:')
59
+ @fingerprint ||= self.class.format_fingerprint(Digest::SHA1.hexdigest(certificate.to_der))
56
60
  end
57
61
 
58
62
  def build(builder)
@@ -0,0 +1,48 @@
1
+ require 'saml2/base'
2
+ require 'saml2/namespaces'
3
+
4
+ module SAML2
5
+ class LocalizedName < Hash
6
+ attr_reader :element
7
+
8
+ def initialize(element, name = nil)
9
+ @element = element
10
+ unless name.nil?
11
+ if name.is_a?(Hash)
12
+ replace(name)
13
+ else
14
+ self[nil] = name
15
+ end
16
+ end
17
+ end
18
+
19
+ def [](lang)
20
+ case lang
21
+ when :all
22
+ self
23
+ when nil
24
+ !empty? && first.last
25
+ else
26
+ super(lang.to_sym)
27
+ end
28
+ end
29
+
30
+ def to_s
31
+ self[nil].to_s
32
+ end
33
+
34
+ def from_xml(nodes)
35
+ clear
36
+ nodes.each do |node|
37
+ self[node['xml:lang'].to_sym] = node.content && node.content.strip
38
+ end
39
+ self
40
+ end
41
+
42
+ def build(builder)
43
+ each do |lang, value|
44
+ builder['md'].__send__(element, value, 'xml:lang' => lang)
45
+ end
46
+ end
47
+ end
48
+ end
data/lib/saml2/message.rb CHANGED
@@ -2,6 +2,7 @@ require 'securerandom'
2
2
  require 'time'
3
3
 
4
4
  require 'saml2/base'
5
+ require 'saml2/signable'
5
6
 
6
7
  module SAML2
7
8
  class InvalidMessage < RuntimeError
@@ -38,8 +39,9 @@ module SAML2
38
39
  # ancestor, but they have several things in common so it's useful to represent
39
40
  # that here
40
41
  class Message < Base
41
- attr_reader :id, :issue_instant
42
- attr_accessor :issuer, :destination
42
+ include Signable
43
+
44
+ attr_writer :issuer, :destination
43
45
 
44
46
  class << self
45
47
  def inherited(klass)
@@ -86,14 +88,41 @@ module SAML2
86
88
  true
87
89
  end
88
90
 
89
- def issuer
90
- @issuer ||= NameID.from_xml(xml.at_xpath('saml:Issuer', Namespaces::ALL))
91
+ def validate_signature(fingerprint: nil, cert: nil, verification_time: nil)
92
+ # verify the signature (certificate's validity) as of the time the message was generated
93
+ super(fingerprint: fingerprint, cert: cert, verification_time: issue_instant)
94
+ end
95
+
96
+ def sign(x509_certificate, private_key, algorithm_name = :sha256)
97
+ super
98
+
99
+ xml = @document.root
100
+ # the Signature element must be right after the Issuer, so put it there
101
+ issuer = xml.at_xpath("saml:Issuer", Namespaces::ALL)
102
+ signature = xml.at_xpath("dsig:Signature", Namespaces::ALL)
103
+ issuer.add_next_sibling(signature)
104
+ self
91
105
  end
92
106
 
93
107
  def id
94
108
  @id ||= xml['ID']
95
109
  end
96
110
 
111
+ def issue_instant
112
+ @issue_instant ||= Time.parse(xml['IssueInstant'])
113
+ end
114
+
115
+ def destination
116
+ if xml && !instance_variable_defined?(:@destination)
117
+ @destination = xml['Destination']
118
+ end
119
+ @destination
120
+ end
121
+
122
+ def issuer
123
+ @issuer ||= NameID.from_xml(xml.at_xpath('saml:Issuer', Namespaces::ALL))
124
+ end
125
+
97
126
  protected
98
127
 
99
128
  # should be called from inside the specific request element
@@ -1,73 +1,28 @@
1
+ require 'saml2/base'
2
+ require 'saml2/localized_name'
1
3
  require 'saml2/namespaces'
2
4
 
3
5
  module SAML2
4
- class Organization
5
- def self.from_xml(node)
6
- return nil unless node
6
+ class Organization < Base
7
+ attr_reader :name, :display_name, :url
7
8
 
8
- new(nodes_to_hash(node.xpath('md:OrganizationName', Namespaces::ALL)),
9
- nodes_to_hash(node.xpath('md:OrganizationDisplayName', Namespaces::ALL)),
10
- nodes_to_hash(node.xpath('md:OrganizationURL', Namespaces::ALL)))
9
+ def from_xml(node)
10
+ name.from_xml(node.xpath('md:OrganizationName', Namespaces::ALL))
11
+ display_name.from_xml(node.xpath('md:OrganizationDisplayName', Namespaces::ALL))
12
+ url.from_xml(node.xpath('md:OrganizationURL', Namespaces::ALL))
11
13
  end
12
14
 
13
- def initialize(name, display_name, url)
14
- if !name.is_a?(Hash)
15
- name = { nil => name}
16
- end
17
- if !display_name.is_a?(Hash)
18
- display_name = { nil => display_name }
19
- end
20
- if !url.is_a?(Hash)
21
- url = { nil => url }
22
- end
23
-
24
- @name, @display_name, @url = name, display_name, url
25
- end
26
-
27
- def name(lang = nil)
28
- self.class.localized_name(@name, lang)
29
- end
30
-
31
- def display_name(lang = nil)
32
- self.class.localized_name(@display_name, lang)
33
- end
34
-
35
- def url(lang = nil)
36
- self.class.localized_name(@url, lang)
15
+ def initialize(name = nil, display_name = nil, url = nil)
16
+ @name = LocalizedName.new('OrganizationName', name)
17
+ @display_name = LocalizedName.new('OrganizationDisplayName', display_name)
18
+ @url = LocalizedName.new('OrganizationURL', url)
37
19
  end
38
20
 
39
21
  def build(builder)
40
22
  builder['md'].Organization do |organization|
41
- self.class.build(organization, @name, 'OrganizationName')
42
- self.class.build(organization, @display_name, 'OrganizationDisplayName')
43
- self.class.build(organization, @url, 'OrganizationURL')
44
- end
45
- end
46
-
47
- private
48
-
49
- def self.build(builder, hash, element)
50
- hash.each do |lang, value|
51
- builder['md'].__send__(element, value, 'xml:lang' => lang)
52
- end
53
- end
54
-
55
- def self.nodes_to_hash(nodes)
56
- hash = {}
57
- nodes.each do |node|
58
- hash[node['xml:lang'].to_sym] = node.content && node.content.strip
59
- end
60
- hash
61
- end
62
-
63
- def self.localized_name(hash, lang)
64
- case lang
65
- when :all
66
- hash
67
- when nil
68
- !hash.empty? && hash.first.last
69
- else
70
- hash[lang.to_sym]
23
+ @name.build(organization)
24
+ @display_name.build(organization)
25
+ @url.build(organization)
71
26
  end
72
27
  end
73
28
  end
@@ -57,6 +57,18 @@ module SAML2
57
57
  @assertions = []
58
58
  end
59
59
 
60
+ def from_xml(node)
61
+ super
62
+ remove_instance_variable(:@assertions)
63
+ end
64
+
65
+ def assertions
66
+ unless instance_variable_defined?(:@assertions)
67
+ @assertions = load_object_array(xml, 'saml:Assertion', Assertion)
68
+ end
69
+ @assertions
70
+ end
71
+
60
72
  def sign(*args)
61
73
  assertions.each { |assertion| assertion.sign(*args) }
62
74
  end
@@ -71,7 +83,10 @@ module SAML2
71
83
  super(response)
72
84
 
73
85
  assertions.each do |assertion|
74
- response.parent << assertion.to_xml
86
+ # we can't just call build, because it may already
87
+ # be signed as a separate message, so call to_xml to
88
+ # get the cached signed result
89
+ response.parent << assertion.to_xml.root
75
90
  end
76
91
  end
77
92
  end
data/lib/saml2/role.rb CHANGED
@@ -1,8 +1,9 @@
1
1
  require 'set'
2
2
 
3
3
  require 'saml2/base'
4
- require 'saml2/organization_and_contacts'
5
4
  require 'saml2/key'
5
+ require 'saml2/organization_and_contacts'
6
+ require 'saml2/signable'
6
7
 
7
8
  module SAML2
8
9
  class Role < Base
@@ -11,6 +12,7 @@ module SAML2
11
12
  end
12
13
 
13
14
  include OrganizationAndContacts
15
+ include Signable
14
16
 
15
17
  attr_writer :supported_protocols, :keys
16
18
 
@@ -38,6 +38,10 @@ module SAML2
38
38
  assertion_consumer_services.each do |acs|
39
39
  acs.build(sp_sso_descriptor, 'AssertionConsumerService')
40
40
  end
41
+
42
+ attribute_consuming_services.each do |acs|
43
+ acs.build(sp_sso_descriptor)
44
+ end
41
45
  end
42
46
  end
43
47
  end
@@ -0,0 +1,69 @@
1
+ require 'saml2/key'
2
+
3
+ module SAML2
4
+ module Signable
5
+ def signature
6
+ unless instance_variable_defined?(:@signature)
7
+ @signature = xml.at_xpath('dsig:Signature', Namespaces::ALL)
8
+ if @signature
9
+ signed_node = @signature.at_xpath('dsig:SignedInfo/dsig:Reference', Namespaces::ALL)['URI']
10
+ if signed_node == ''
11
+ @signature = nil unless xml == xml.document.root
12
+ elsif signed_node != "##{xml['ID']}"
13
+ # validating the schema will automatically add ID attributes, so check that first
14
+ xml.set_id_attribute('ID') unless xml.document.get_id(xml['ID'])
15
+ @signature = nil
16
+ end
17
+ end
18
+ end
19
+ @signature
20
+ end
21
+
22
+ def signing_key
23
+ @signing_key ||= Key.from_xml(signature)
24
+ end
25
+
26
+ def signed?
27
+ !!signature
28
+ end
29
+
30
+ def validate_signature(fingerprint: nil, cert: nil, verification_time: nil)
31
+ return ["not signed"] unless signed?
32
+
33
+ certs = Array(cert)
34
+ # see if any given fingerprints match the certificate embedded in the XML;
35
+ # if so, extract the certificate, and add it to the allowed certificates list
36
+ Array(fingerprint)&.each do |fp|
37
+ certs << signing_key.certificate if signing_key&.fingerprint == Key.format_fingerprint(fp)
38
+ end
39
+ certs = certs.uniq
40
+ return ["no certificate found"] if certs.empty?
41
+
42
+ begin
43
+ # verify_certificates being false is hopefully a temporary thing, until I can figure
44
+ # out how to get xmlsec to root a trust chain in a non-root certificate
45
+ result = signature.verify_with(certs: certs, verification_time: verification_time, verify_certificates: false)
46
+ result ? [] : ["signature does not match"]
47
+ rescue XMLSec::VerificationError => e
48
+ [e.message]
49
+ end
50
+ end
51
+
52
+ def valid_signature?(fingerprint: nil, cert: nil, verification_time: nil)
53
+ validate_signature(fingerprint: fingerprint, cert: cert, verification_time: verification_time).empty?
54
+ end
55
+
56
+ def sign(x509_certificate, private_key, algorithm_name = :sha256)
57
+ to_xml
58
+
59
+ xml = @document.root
60
+ xml.set_id_attribute('ID')
61
+ xml.sign!(cert: x509_certificate, key: private_key, digest_alg: algorithm_name.to_s, signature_alg: "rsa-#{algorithm_name}", uri: "##{id}")
62
+ # the Signature element must be the first element
63
+ signature = xml.at_xpath("dsig:Signature", Namespaces::ALL)
64
+ xml.children.first.add_previous_sibling(signature)
65
+
66
+ self
67
+ end
68
+ end
69
+ end
data/lib/saml2/status.rb CHANGED
@@ -2,7 +2,7 @@ require 'saml2/base'
2
2
 
3
3
  module SAML2
4
4
  class Status < Base
5
- SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success".freeze
5
+ SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success".freeze
6
6
 
7
7
  attr_accessor :code, :message
8
8
 
@@ -16,6 +16,10 @@ module SAML2
16
16
  self.message = load_string_array(xml, 'samlp:StatusMessage')
17
17
  end
18
18
 
19
+ def success?
20
+ code == SUCCESS
21
+ end
22
+
19
23
  def build(builder)
20
24
  builder['samlp'].Status do |status|
21
25
  status['samlp'].StatusCode(Value: code)
data/lib/saml2/subject.rb CHANGED
@@ -4,19 +4,42 @@ require 'saml2/namespaces'
4
4
  module SAML2
5
5
  class Subject < Base
6
6
  attr_writer :name_id
7
- attr_accessor :confirmation
7
+ attr_writer :confirmations
8
+
9
+ def initialize
10
+ @confirmations = []
11
+ end
12
+
13
+ def from_xml(node)
14
+ super
15
+ @confirmations = nil
16
+ end
8
17
 
9
18
  def name_id
10
19
  if xml && !instance_variable_defined?(:@name_id)
11
- @name_id = NameID.from_xml(node.at_xpath('saml:NameID', Namespaces::ALL))
20
+ @name_id = NameID.from_xml(xml.at_xpath('saml:NameID', Namespaces::ALL))
12
21
  end
13
22
  @name_id
14
23
  end
15
24
 
25
+ def confirmation
26
+ Array.wrap(confirmations).first
27
+ end
28
+
29
+ def confirmation=(value)
30
+ @confirmations = [value]
31
+ end
32
+
33
+ def confirmations
34
+ @confirmations ||= load_object_array(xml, 'saml:SubjectConfirmation', Confirmation)
35
+ end
36
+
16
37
  def build(builder)
17
38
  builder['saml'].Subject do |subject|
18
39
  name_id.build(subject) if name_id
19
- confirmation.build(subject) if confirmation
40
+ Array(confirmations).each do |confirmation|
41
+ confirmation.build(subject)
42
+ end
20
43
  end
21
44
  end
22
45
 
@@ -29,6 +52,18 @@ module SAML2
29
52
 
30
53
  attr_accessor :method, :not_before, :not_on_or_after, :recipient, :in_response_to
31
54
 
55
+ def from_xml(node)
56
+ super
57
+ self.method = node['Method']
58
+ confirmation_data = node.at_xpath('saml:SubjectConfirmationData', Namespaces::ALL)
59
+ if confirmation_data
60
+ self.not_before = Time.parse(confirmation_data['NotBefore']) if confirmation_data['NotBefore']
61
+ self.not_on_or_after = Time.parse(confirmation_data['NotOnOrAfter']) if confirmation_data['NotOnOrAfter']
62
+ self.recipient = confirmation_data['Recipient']
63
+ self.in_response_to = confirmation_data['InResponseTo']
64
+ end
65
+ end
66
+
32
67
  def build(builder)
33
68
  builder['saml'].SubjectConfirmation('Method' => method) do |subject_confirmation|
34
69
  if in_response_to ||
data/lib/saml2/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module SAML2
2
- VERSION = '1.1.5'
2
+ VERSION = '2.0.0'
3
3
  end
@@ -1,5 +1,5 @@
1
1
  <?xml version="1.0"?>
2
- <EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="http://siteadmin.instructure.com/saml2">
2
+ <EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="http://siteadmin.instructure.com/saml2" ID="unique">
3
3
  <SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
4
4
 
5
5
  <KeyDescriptor use="encryption">
@@ -5,24 +5,6 @@ module SAML2
5
5
  let(:sp) { Entity.parse(fixture('service_provider.xml')).roles.first }
6
6
  let(:request) { AuthnRequest.parse(fixture('authnrequest.xml')) }
7
7
 
8
- describe '.decode' do
9
- it "should not choke on empty string" do
10
- authnrequest = AuthnRequest.decode('')
11
- expect(authnrequest.valid_schema?).to eq false
12
- end
13
-
14
- it "should not choke on garbage" do
15
- authnrequest = AuthnRequest.decode('abc')
16
- expect(authnrequest.valid_schema?).to eq false
17
- end
18
-
19
- it "properly handles authnrequests that have pluses in them" do
20
- samlrequest = "hZJbU8IwEIX/Smbfe6H1mqE4COPIDGoHqg++hXShmWkTzKao/95QQNEHfN09J2f32/RvPpqabdCSMjqDXhgDQy1NqfQqg+fiLriCm0GfRFMnaz5sXaVn+NYiOeaNmviuk0FrNTeCFHEtGiTuJJ8PH6Y8CWO+tsYZaWpgQyK0zkeNjKa2QTtHu1ESn2fTDCrn1sSjSJqmabVyn6EUeiOobij0tWgbFREZYGOfr7Rw3cwHm+/8MWwHSKKpWSkN7M5Yid0CGSxFTQhsMs5ApCotqzKRWEmxqha91VVVxvIMy1TGl8qLKBdEaoM/NqIWJ5qc0C6DJO5dBvFFkJwVvXOepDy9DtPr+BVYvl/7VukdzlOMFjsR8fuiyIP8aV4AezmcxQtgfwTepdtj+qcfFgfkMPgHcD86Tvg++qN/cjLOTa3kJxvWtXkfWRTO83C2xQ5sI9zpIbYVVQbLTsrX273IoXYQDfapvz/X4As="
21
- authnrequest = AuthnRequest.decode(samlrequest)
22
- expect(authnrequest.valid_schema?).to eq true
23
- end
24
- end
25
-
26
8
  it "should be valid" do
27
9
  expect(request.valid_schema?).to eq true
28
10
  expect(request.resolve(sp)).to eq true
@@ -29,8 +29,9 @@ module SAML2
29
29
  end
30
30
 
31
31
  it "doesn't allow deflate bombs" do
32
- message = "\0" * 2 * 1024 * 1024
32
+ message = double()
33
33
  allow(message).to receive(:destination).and_return("http://somewhere/")
34
+ allow(message).to receive(:to_s).and_return("\0" * 2 * 1024 * 1024)
34
35
  url = Bindings::HTTPRedirect.encode(message)
35
36
 
36
37
  expect { Bindings::HTTPRedirect.decode(url) }.to raise_error(MessageTooLarge)
@@ -47,16 +48,18 @@ module SAML2
47
48
  end
48
49
 
49
50
  it "validates encoding" do
50
- message = "hi"
51
+ message = double()
51
52
  allow(message).to receive(:destination).and_return("http://somewhere/")
53
+ allow(message).to receive(:to_s).and_return("hi")
52
54
  url = Bindings::HTTPRedirect.encode(message, relay_state: "abc")
53
55
  url << "&SAMLEncoding=garbage"
54
56
  expect { Bindings::HTTPRedirect.decode(url) }.to raise_error(UnsupportedEncoding)
55
57
  end
56
58
 
57
59
  it "returns relay state" do
58
- message = "hi"
60
+ message = double()
59
61
  allow(message).to receive(:destination).and_return("http://somewhere/")
62
+ allow(message).to receive(:to_s).and_return("hi")
60
63
  url = Bindings::HTTPRedirect.encode(message, relay_state: "abc")
61
64
  allow(Message).to receive(:parse).with("hi").and_return("parsed")
62
65
  message, relay_state = Bindings::HTTPRedirect.decode(url)
@@ -88,8 +91,9 @@ module SAML2
88
91
  end
89
92
 
90
93
  it "allows the caller to detect an unsigned message" do
91
- message = "hi"
94
+ message = double()
92
95
  allow(message).to receive(:destination).and_return("http://somewhere/")
96
+ allow(message).to receive(:to_s).and_return("hi")
93
97
  url = Bindings::HTTPRedirect.encode(message)
94
98
  allow(Message).to receive(:parse).with("hi").and_return("parsed")
95
99
 
@@ -102,8 +106,9 @@ module SAML2
102
106
  end
103
107
 
104
108
  it "requires a signature if a key is passed" do
105
- message = "hi"
109
+ message = double()
106
110
  allow(message).to receive(:destination).and_return("http://somewhere/")
111
+ allow(message).to receive(:to_s).and_return("hi")
107
112
  url = Bindings::HTTPRedirect.encode(message)
108
113
  allow(Message).to receive(:parse).with("hi").and_return("parsed")
109
114
 
@@ -127,15 +132,17 @@ module SAML2
127
132
 
128
133
  describe '.encode' do
129
134
  it 'works' do
130
- message = "hi"
135
+ message = double()
131
136
  allow(message).to receive(:destination).and_return("http://somewhere/")
137
+ allow(message).to receive(:to_s).and_return("hi")
132
138
  url = Bindings::HTTPRedirect.encode(message, relay_state: "abc")
133
139
  expect(url).to match(%r{^http://somewhere/\?SAMLResponse=(?:.*)&RelayState=abc})
134
140
  end
135
141
 
136
142
  it 'signs a message' do
137
- message = "hi"
143
+ message = double()
138
144
  allow(message).to receive(:destination).and_return("http://somewhere/")
145
+ allow(message).to receive(:to_s).and_return("hi")
139
146
  key = OpenSSL::PKey::RSA.new(fixture('privatekey.key'))
140
147
  url = Bindings::HTTPRedirect.encode(message, relay_state: "abc", private_key: key)
141
148
 
@@ -25,10 +25,10 @@ module SAML2
25
25
  end
26
26
 
27
27
  it "should parse the organization" do
28
- expect(entity.organization.display_name).to eq 'Canvas'
29
- expect(entity.organization.display_name('en')).to eq 'Canvas'
30
- expect(entity.organization.display_name('es')).to be_nil
31
- expect(entity.organization.display_name(:all)).to eq en: 'Canvas'
28
+ expect(entity.organization.display_name.to_s).to eq 'Canvas'
29
+ expect(entity.organization.display_name['en']).to eq 'Canvas'
30
+ expect(entity.organization.display_name['es']).to be_nil
31
+ expect(entity.organization.display_name[:all]).to eq en: 'Canvas'
32
32
  end
33
33
 
34
34
  it "validates metadata from ADFS containing lots of non-SAML schemas" do
@@ -36,6 +36,13 @@ module SAML2
36
36
  end
37
37
  end
38
38
 
39
+ it "should sign correctly" do
40
+ entity = Entity.parse(fixture('service_provider.xml'))
41
+ entity.sign(fixture('certificate.pem'), fixture('privatekey.key'))
42
+ entity2 = Entity.parse(entity.to_s)
43
+ expect(entity2.valid_schema?).to eq true
44
+ end
45
+
39
46
  describe Entity::Group do
40
47
  it "should parse and validate" do
41
48
  group = Entity.parse(fixture('entities.xml'))
@@ -42,14 +42,14 @@ module SAML2
42
42
  expect(Schemas.protocol.validate(response.to_xml)).to eq []
43
43
  # verifiable on the command line with:
44
44
  # xmlsec1 --verify --pubkey-cert-pem certificate.pem --privkey-pem privatekey.key --id-attr:ID urn:oasis:names:tc:SAML:2.0:assertion:Assertion response_signed.xml
45
- expect(response.to_s).to eq fixture('response_signed.xml')
45
+ expect(response.to_s(pretty: false)).to eq fixture('response_signed.xml')
46
46
  end
47
47
 
48
48
  it "should generate a valid signature when attributes are present" do
49
49
  freeze_response
50
50
  response.assertions.first.statements << sp.attribute_consuming_services.default.create_statement('givenName' => 'cody')
51
51
  response.sign(fixture('certificate.pem'), fixture('privatekey.key'))
52
- expect(response.to_s).to eq fixture('response_with_attribute_signed.xml')
52
+ expect(response.to_s(pretty: false)).to eq fixture('response_with_attribute_signed.xml')
53
53
  end
54
54
 
55
55
  it "should generate valid XML for IdP initiated response" do
@@ -57,5 +57,11 @@ module SAML2
57
57
  NameID.new('jacob', NameID::Format::PERSISTENT))
58
58
  expect(Schemas.protocol.validate(Nokogiri::XML(response.to_s))).to eq []
59
59
  end
60
+
61
+ it "parses a serialized assertion" do
62
+ response2 = Message.parse(response.to_s)
63
+ expect(response2.assertions.length).to eq 1
64
+ expect(response2.assertions.first.subject.name_id.id).to eq 'jacob'
65
+ end
60
66
  end
61
67
  end
@@ -14,10 +14,14 @@ module SAML2
14
14
  sp = ServiceProvider.new
15
15
  sp.single_logout_services << Endpoint.new('https://sso.canvaslms.com/SAML2/Logout',
16
16
  Bindings::HTTPRedirect::URN)
17
- sp.assertion_consumer_services << Endpoint::Indexed.new('https://sso.canvaslms.com/SAML2/Login1', 0)
18
- sp.assertion_consumer_services << Endpoint::Indexed.new('https://sso.canvaslms.com/SAML2/Login2', 1)
17
+ sp.assertion_consumer_services << Endpoint::Indexed.new('https://sso.canvaslms.com/SAML2/Login1')
18
+ sp.assertion_consumer_services << Endpoint::Indexed.new('https://sso.canvaslms.com/SAML2/Login2')
19
19
  sp.keys << Key.new('somedata', Key::Type::ENCRYPTION, [Key::EncryptionMethod.new])
20
20
  sp.keys << Key.new('somedata', Key::Type::SIGNING)
21
+ acs = AttributeConsumingService.new
22
+ acs.name[:en] = 'service'
23
+ acs.requested_attributes << RequestedAttribute.create('uid')
24
+ sp.attribute_consuming_services << acs
21
25
 
22
26
  entity.roles << sp
23
27
  expect(Schemas.metadata.validate(Nokogiri::XML(entity.to_s))).to eq []
@@ -38,7 +42,7 @@ module SAML2
38
42
  end
39
43
 
40
44
  it "should load the organization" do
41
- expect(entity.organization.display_name).to eq 'Canvas'
45
+ expect(entity.organization.display_name.to_s).to eq 'Canvas'
42
46
  end
43
47
 
44
48
  it "should load contacts" do
@@ -46,6 +50,15 @@ module SAML2
46
50
  expect(entity.contacts.first.type).to eq Contact::Type::TECHNICAL
47
51
  expect(entity.contacts.first.surname).to eq 'Administrator'
48
52
  end
53
+
54
+ it "loads attribute_consuming_services" do
55
+ expect(sp.attribute_consuming_services.length).to eq 1
56
+ acs = sp.attribute_consuming_services.first
57
+ expect(acs.index).to eq 0
58
+ expect(acs.name.to_s).to eq 'service'
59
+ expect(acs.requested_attributes.length).to eq 1
60
+ expect(acs.requested_attributes.first.name).to eq 'urn:oid:2.5.4.42'
61
+ end
49
62
  end
50
63
  end
51
64
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: saml2
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.5
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-11-16 00:00:00.000000000 Z
11
+ date: 2017-12-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -147,6 +147,7 @@ files:
147
147
  - lib/saml2/identity_provider.rb
148
148
  - lib/saml2/indexed_object.rb
149
149
  - lib/saml2/key.rb
150
+ - lib/saml2/localized_name.rb
150
151
  - lib/saml2/logout_request.rb
151
152
  - lib/saml2/logout_response.rb
152
153
  - lib/saml2/message.rb
@@ -160,6 +161,7 @@ files:
160
161
  - lib/saml2/role.rb
161
162
  - lib/saml2/schemas.rb
162
163
  - lib/saml2/service_provider.rb
164
+ - lib/saml2/signable.rb
163
165
  - lib/saml2/sso.rb
164
166
  - lib/saml2/status.rb
165
167
  - lib/saml2/status_response.rb
@@ -221,7 +223,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
221
223
  version: '0'
222
224
  requirements: []
223
225
  rubyforge_project:
224
- rubygems_version: 2.5.2
226
+ rubygems_version: 2.6.12
225
227
  signing_key:
226
228
  specification_version: 4
227
229
  summary: SAML 2.0 Library