saml2 1.1.5 → 2.0.0

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