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 +4 -4
- data/lib/saml2/assertion.rb +33 -30
- data/lib/saml2/attribute.rb +9 -2
- data/lib/saml2/attribute_consuming_service.rb +37 -5
- data/lib/saml2/authn_request.rb +0 -9
- data/lib/saml2/authn_statement.rb +14 -2
- data/lib/saml2/base.rb +25 -6
- data/lib/saml2/bindings/http_post.rb +28 -0
- data/lib/saml2/bindings/http_redirect.rb +1 -1
- data/lib/saml2/conditions.rb +27 -3
- data/lib/saml2/endpoint.rb +2 -8
- data/lib/saml2/entity.rb +16 -17
- data/lib/saml2/indexed_object.rb +7 -2
- data/lib/saml2/key.rb +5 -1
- data/lib/saml2/localized_name.rb +48 -0
- data/lib/saml2/message.rb +33 -4
- data/lib/saml2/organization.rb +15 -60
- data/lib/saml2/response.rb +16 -1
- data/lib/saml2/role.rb +3 -1
- data/lib/saml2/service_provider.rb +4 -0
- data/lib/saml2/signable.rb +69 -0
- data/lib/saml2/status.rb +5 -1
- data/lib/saml2/subject.rb +38 -3
- data/lib/saml2/version.rb +1 -1
- data/spec/fixtures/service_provider.xml +1 -1
- data/spec/lib/authn_request_spec.rb +0 -18
- data/spec/lib/bindings/http_redirect_spec.rb +14 -7
- data/spec/lib/entity_spec.rb +11 -4
- data/spec/lib/response_spec.rb +8 -2
- data/spec/lib/service_provider_spec.rb +16 -3
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 49552e3c1623dc97bebb250e582f14925c243a01
|
4
|
+
data.tar.gz: ec92142eee88b9f0bb7b53cb747ca7c9c874ea52
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 22a08e29c00544c48402463bf5615829348abffc0ca79b823f8d594480a422af4ecef8e49cd934a6898d16c5f6dcb890b3183c6fc8d62d0796f21532d87a6778
|
7
|
+
data.tar.gz: a000f59983fff2fc9c2b6385d5a2f94076f15fee45503c8b58de2c2647b112fa954503c3023ca2baf5c049e493f023e386524b8646ff7b254090eb2119f7503c
|
data/lib/saml2/assertion.rb
CHANGED
@@ -1,45 +1,48 @@
|
|
1
1
|
require 'saml2/conditions'
|
2
2
|
|
3
3
|
module SAML2
|
4
|
-
class Assertion
|
5
|
-
|
6
|
-
attr_accessor :issuer, :subject
|
4
|
+
class Assertion < Message
|
5
|
+
attr_writer :statements, :subject
|
7
6
|
|
8
7
|
def initialize
|
9
|
-
|
10
|
-
@issue_instant = Time.now.utc
|
8
|
+
super
|
11
9
|
@statements = []
|
12
10
|
@conditions = Conditions.new
|
13
11
|
end
|
14
12
|
|
15
|
-
def
|
16
|
-
|
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
|
-
|
19
|
-
@
|
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
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
data/lib/saml2/attribute.rb
CHANGED
@@ -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[
|
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
|
-
|
10
|
-
|
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
|
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
|
-
|
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
|
data/lib/saml2/authn_request.rb
CHANGED
@@ -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
|
-
|
20
|
-
|
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.
|
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
|
data/lib/saml2/conditions.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/saml2/endpoint.rb
CHANGED
@@ -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 =
|
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 =
|
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
|
57
|
-
|
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
|
data/lib/saml2/indexed_object.rb
CHANGED
@@ -2,7 +2,7 @@ require 'saml2/base'
|
|
2
2
|
|
3
3
|
module SAML2
|
4
4
|
module IndexedObject
|
5
|
-
|
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
|
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)
|
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
|
-
|
42
|
-
|
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
|
90
|
-
|
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
|
data/lib/saml2/organization.rb
CHANGED
@@ -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
|
-
|
6
|
-
return nil unless node
|
6
|
+
class Organization < Base
|
7
|
+
attr_reader :name, :display_name, :url
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
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
|
data/lib/saml2/response.rb
CHANGED
@@ -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
|
-
|
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
|
|
@@ -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
|
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
|
-
|
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(
|
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
|
-
|
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,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 =
|
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 =
|
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 =
|
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 =
|
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 =
|
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 =
|
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 =
|
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
|
|
data/spec/lib/entity_spec.rb
CHANGED
@@ -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
|
30
|
-
expect(entity.organization.display_name
|
31
|
-
expect(entity.organization.display_name
|
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'))
|
data/spec/lib/response_spec.rb
CHANGED
@@ -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'
|
18
|
-
sp.assertion_consumer_services << Endpoint::Indexed.new('https://sso.canvaslms.com/SAML2/Login2'
|
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:
|
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
|
+
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.
|
226
|
+
rubygems_version: 2.6.12
|
225
227
|
signing_key:
|
226
228
|
specification_version: 4
|
227
229
|
summary: SAML 2.0 Library
|