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 +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
|