saml2 3.1.2 → 3.1.4
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/Rakefile +6 -4
- data/exe/bulk_verify_responses +94 -0
- data/lib/saml2/assertion.rb +7 -7
- data/lib/saml2/attribute/x500.rb +31 -28
- data/lib/saml2/attribute.rb +53 -49
- data/lib/saml2/attribute_consuming_service.rb +29 -31
- data/lib/saml2/authn_request.rb +54 -47
- data/lib/saml2/authn_statement.rb +31 -20
- data/lib/saml2/base.rb +72 -63
- data/lib/saml2/bindings/http_post.rb +7 -7
- data/lib/saml2/bindings/http_redirect.rb +37 -33
- data/lib/saml2/bindings.rb +1 -1
- data/lib/saml2/conditions.rb +19 -16
- data/lib/saml2/contact.rb +19 -18
- data/lib/saml2/endpoint.rb +14 -11
- data/lib/saml2/entity.rb +27 -27
- data/lib/saml2/identity_provider.rb +13 -10
- data/lib/saml2/indexed_object.rb +15 -12
- data/lib/saml2/key.rb +43 -34
- data/lib/saml2/localized_name.rb +11 -10
- data/lib/saml2/logout_request.rb +8 -8
- data/lib/saml2/logout_response.rb +4 -4
- data/lib/saml2/message.rb +24 -20
- data/lib/saml2/name_id.rb +45 -41
- data/lib/saml2/namespaces.rb +8 -8
- data/lib/saml2/organization.rb +11 -10
- data/lib/saml2/organization_and_contacts.rb +5 -5
- data/lib/saml2/request.rb +3 -3
- data/lib/saml2/requested_authn_context.rb +4 -4
- data/lib/saml2/response.rb +45 -33
- data/lib/saml2/role.rb +11 -11
- data/lib/saml2/schemas.rb +13 -10
- data/lib/saml2/service_provider.rb +11 -12
- data/lib/saml2/signable.rb +23 -18
- data/lib/saml2/sso.rb +5 -5
- data/lib/saml2/status.rb +9 -7
- data/lib/saml2/status_response.rb +5 -5
- data/lib/saml2/subject.rb +28 -28
- data/lib/saml2/version.rb +1 -1
- data/lib/saml2.rb +7 -7
- metadata +78 -122
- data/spec/fixtures/FederationMetadata.xml +0 -670
- data/spec/fixtures/authnrequest.xml +0 -12
- data/spec/fixtures/certificate.pem +0 -24
- data/spec/fixtures/entities.xml +0 -13
- data/spec/fixtures/external-uri-reference-response.xml +0 -48
- data/spec/fixtures/identity_provider.xml +0 -46
- data/spec/fixtures/noconditions_response.xml +0 -1
- data/spec/fixtures/othercertificate.pem +0 -25
- data/spec/fixtures/privatekey.key +0 -27
- data/spec/fixtures/response_assertion_signed_reffed_from_response.xml +0 -6
- data/spec/fixtures/response_signed.xml +0 -46
- data/spec/fixtures/response_tampered_certificate.xml +0 -25
- data/spec/fixtures/response_tampered_signature.xml +0 -46
- data/spec/fixtures/response_with_attribute_signed.xml +0 -46
- data/spec/fixtures/response_with_encrypted_assertion.xml +0 -58
- data/spec/fixtures/response_with_rsa_key_value.xml +0 -1
- data/spec/fixtures/response_with_signed_assertion_and_encrypted_subject.xml +0 -116
- data/spec/fixtures/response_without_keyinfo.xml +0 -1
- data/spec/fixtures/service_provider.xml +0 -79
- data/spec/fixtures/test3-response.xml +0 -9
- data/spec/fixtures/test6-response.xml +0 -10
- data/spec/fixtures/test7-response.xml +0 -10
- data/spec/fixtures/xml_missigned_assertion.xml +0 -84
- data/spec/fixtures/xml_signature_wrapping_attack_duplicate_ids.xml +0 -11
- data/spec/fixtures/xml_signature_wrapping_attack_response_attributes.xml +0 -45
- data/spec/fixtures/xml_signature_wrapping_attack_response_nameid.xml +0 -44
- data/spec/fixtures/xslt-transform-response.xml +0 -57
- data/spec/lib/attribute_consuming_service_spec.rb +0 -129
- data/spec/lib/attribute_spec.rb +0 -149
- data/spec/lib/authn_request_spec.rb +0 -52
- data/spec/lib/bindings/http_redirect_spec.rb +0 -183
- data/spec/lib/conditions_spec.rb +0 -74
- data/spec/lib/entity_spec.rb +0 -58
- data/spec/lib/identity_provider_spec.rb +0 -43
- data/spec/lib/indexed_object_spec.rb +0 -71
- data/spec/lib/key_spec.rb +0 -23
- data/spec/lib/logout_request_spec.rb +0 -33
- data/spec/lib/logout_response_spec.rb +0 -33
- data/spec/lib/message_spec.rb +0 -23
- data/spec/lib/response_spec.rb +0 -293
- data/spec/lib/service_provider_spec.rb +0 -76
- data/spec/lib/signable_spec.rb +0 -15
- data/spec/spec_helper.rb +0 -8
data/lib/saml2/authn_request.rb
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
10
|
-
require
|
11
|
-
require
|
12
|
-
require
|
13
|
-
require
|
14
|
-
require
|
3
|
+
require "base64"
|
4
|
+
require "zlib"
|
5
|
+
|
6
|
+
require "saml2/attribute_consuming_service"
|
7
|
+
require "saml2/bindings/http_redirect"
|
8
|
+
require "saml2/endpoint"
|
9
|
+
require "saml2/name_id"
|
10
|
+
require "saml2/namespaces"
|
11
|
+
require "saml2/request"
|
12
|
+
require "saml2/requested_authn_context"
|
13
|
+
require "saml2/schemas"
|
14
|
+
require "saml2/subject"
|
15
15
|
|
16
16
|
module SAML2
|
17
17
|
class AuthnRequest < Request
|
@@ -35,8 +35,8 @@ module SAML2
|
|
35
35
|
# @param service_provider [ServiceProvider]
|
36
36
|
# @return [AuthnRequest]
|
37
37
|
def self.initiate(issuer, identity_provider = nil,
|
38
|
-
|
39
|
-
|
38
|
+
assertion_consumer_service: nil,
|
39
|
+
service_provider: nil)
|
40
40
|
authn_request = new
|
41
41
|
authn_request.issuer = issuer
|
42
42
|
authn_request.destination = identity_provider.single_sign_on_services.first.location if identity_provider
|
@@ -80,12 +80,16 @@ module SAML2
|
|
80
80
|
def resolve(service_provider)
|
81
81
|
# TODO: check signature if present
|
82
82
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
83
|
+
@assertion_consumer_service =
|
84
|
+
if assertion_consumer_service_url
|
85
|
+
service_provider.assertion_consumer_services.find do |acs|
|
86
|
+
acs.location == assertion_consumer_service_url
|
87
|
+
end
|
88
|
+
else
|
89
|
+
service_provider.assertion_consumer_services.resolve(assertion_consumer_service_index)
|
90
|
+
end
|
91
|
+
@attribute_consuming_service =
|
92
|
+
service_provider.attribute_consuming_services.resolve(attribute_consuming_service_index)
|
89
93
|
|
90
94
|
return false unless @assertion_consumer_service
|
91
95
|
return false if attribute_consuming_service_index && !@attribute_consuming_service
|
@@ -96,7 +100,7 @@ module SAML2
|
|
96
100
|
# @return [NameID::Policy, nil]
|
97
101
|
def name_id_policy
|
98
102
|
if xml && !instance_variable_defined?(:@name_id_policy)
|
99
|
-
@name_id_policy = NameID::Policy.from_xml(xml.at_xpath(
|
103
|
+
@name_id_policy = NameID::Policy.from_xml(xml.at_xpath("samlp:NameIDPolicy", Namespaces::ALL))
|
100
104
|
end
|
101
105
|
@name_id_policy
|
102
106
|
end
|
@@ -111,7 +115,7 @@ module SAML2
|
|
111
115
|
# @return [Integer, nil]
|
112
116
|
def assertion_consumer_service_index
|
113
117
|
if xml && !instance_variable_defined?(:@assertion_consumer_service_index)
|
114
|
-
@assertion_consumer_service_index = xml[
|
118
|
+
@assertion_consumer_service_index = xml["AssertionConsumerServiceIndex"]&.to_i
|
115
119
|
end
|
116
120
|
@assertion_consumer_service_index
|
117
121
|
end
|
@@ -119,7 +123,7 @@ module SAML2
|
|
119
123
|
# @return [String, nil]
|
120
124
|
def assertion_consumer_service_url
|
121
125
|
if xml && !instance_variable_defined?(:@assertion_consumer_service_url)
|
122
|
-
@assertion_consumer_service_url = xml[
|
126
|
+
@assertion_consumer_service_url = xml["AssertionConsumerServiceURL"]
|
123
127
|
end
|
124
128
|
@assertion_consumer_service_url
|
125
129
|
end
|
@@ -127,61 +131,64 @@ module SAML2
|
|
127
131
|
# @return [Integer, nil]
|
128
132
|
def attribute_consuming_service_index
|
129
133
|
if xml && !instance_variable_defined?(:@attribute_consuming_service_index)
|
130
|
-
@attribute_consuming_service_index = xml[
|
134
|
+
@attribute_consuming_service_index = xml["AttributeConsumingServiceIndex"]&.to_i
|
131
135
|
end
|
132
136
|
@attribute_consuming_service_index
|
133
137
|
end
|
134
138
|
|
135
139
|
# @return [true, false, nil]
|
136
140
|
def force_authn?
|
137
|
-
if xml && !instance_variable_defined?(:@force_authn)
|
138
|
-
@force_authn = xml['ForceAuthn']&.== 'true'
|
139
|
-
end
|
141
|
+
@force_authn = xml["ForceAuthn"]&.== "true" if xml && !instance_variable_defined?(:@force_authn)
|
140
142
|
@force_authn
|
141
143
|
end
|
142
144
|
|
143
145
|
# @return [true, false, nil]
|
144
146
|
def passive?
|
145
|
-
if xml && !instance_variable_defined?(:@passive)
|
146
|
-
@passive = xml['IsPassive']&.== 'true'
|
147
|
-
end
|
147
|
+
@passive = xml["IsPassive"]&.== "true" if xml && !instance_variable_defined?(:@passive)
|
148
148
|
@passive
|
149
149
|
end
|
150
150
|
|
151
151
|
# @return [String, nil]
|
152
152
|
def protocol_binding
|
153
|
-
if xml && !instance_variable_defined?(:@protocol_binding)
|
154
|
-
@protocol_binding = xml['ProtocolBinding']
|
155
|
-
end
|
153
|
+
@protocol_binding = xml["ProtocolBinding"] if xml && !instance_variable_defined?(:@protocol_binding)
|
156
154
|
@protocol_binding
|
157
155
|
end
|
158
156
|
|
159
157
|
# @return [Subject, nil]
|
160
158
|
def subject
|
161
159
|
if xml && !instance_variable_defined?(:@subject)
|
162
|
-
@subject = Subject.from_xml(xml.at_xpath(
|
160
|
+
@subject = Subject.from_xml(xml.at_xpath("saml:Subject", Namespaces::ALL))
|
163
161
|
end
|
164
162
|
@subject
|
165
163
|
end
|
166
164
|
|
167
165
|
# (see Base#build)
|
168
166
|
def build(builder)
|
169
|
-
builder[
|
170
|
-
|
171
|
-
|
167
|
+
builder["samlp"].AuthnRequest(
|
168
|
+
"xmlns:samlp" => Namespaces::SAMLP,
|
169
|
+
"xmlns:saml" => Namespaces::SAML
|
172
170
|
) do |authn_request|
|
173
171
|
super(authn_request)
|
174
172
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
173
|
+
if assertion_consumer_service_index
|
174
|
+
authn_request.parent["AssertionConsumerServiceIndex"] =
|
175
|
+
assertion_consumer_service_index
|
176
|
+
end
|
177
|
+
if assertion_consumer_service_url
|
178
|
+
authn_request.parent["AssertionConsumerServiceURL"] =
|
179
|
+
assertion_consumer_service_url
|
180
|
+
end
|
181
|
+
if attribute_consuming_service_index
|
182
|
+
authn_request.parent["AttributeConsumingServiceIndex"] =
|
183
|
+
attribute_consuming_service_index
|
184
|
+
end
|
185
|
+
authn_request.parent["ForceAuthn"] = force_authn? unless force_authn?.nil?
|
186
|
+
authn_request.parent["IsPassive"] = passive? unless passive?.nil?
|
187
|
+
authn_request.parent["ProtocolBinding"] = protocol_binding if protocol_binding
|
188
|
+
|
189
|
+
subject&.build(authn_request)
|
190
|
+
name_id_policy&.build(authn_request)
|
191
|
+
requested_authn_context&.build(authn_request)
|
185
192
|
end
|
186
193
|
end
|
187
194
|
end
|
@@ -1,20 +1,30 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "saml2/base"
|
4
4
|
|
5
5
|
module SAML2
|
6
6
|
class AuthnStatement < Base
|
7
7
|
module Classes
|
8
|
-
INTERNET_PROTOCOL
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
8
|
+
INTERNET_PROTOCOL =
|
9
|
+
"urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocol" # IP address
|
10
|
+
INTERNET_PROTOCOL_PASSWORD =
|
11
|
+
"urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword" # IP address, as well as username/password
|
12
|
+
KERBEROS =
|
13
|
+
"urn:oasis:names:tc:SAML:2.0:ac:classes:Kerberos"
|
14
|
+
PASSWORD =
|
15
|
+
"urn:oasis:names:tc:SAML:2.0:ac:classes:Password" # username/password, NOT over SSL
|
16
|
+
PASSWORD_PROTECTED_TRANSPORT =
|
17
|
+
"urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" # username/password over SSL
|
18
|
+
PREVIOUS_SESSION =
|
19
|
+
"urn:oasis:names:tc:SAML:2.0:ac:classes:PreviousSession" # remember me
|
20
|
+
SMARTCARD =
|
21
|
+
"urn:oasis:names:tc:SAML:2.0:ac:classes:Smartcard"
|
22
|
+
SMARTCARD_PKI =
|
23
|
+
"urn:oasis:names:tc:SAML:2.0:ac:classes:SmartcardPKI" # smartcard with a private key on it
|
24
|
+
TLS_CLIENT =
|
25
|
+
"urn:oasis:names:tc:SAML:2.0:ac:classes:TLSClient" # SSL client certificate
|
26
|
+
UNSPECIFIED =
|
27
|
+
"urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified"
|
18
28
|
end
|
19
29
|
|
20
30
|
# @return [Time]
|
@@ -30,19 +40,20 @@ module SAML2
|
|
30
40
|
# (see Base#from_xml)
|
31
41
|
def from_xml(node)
|
32
42
|
super
|
33
|
-
@authn_instant = Time.parse(node[
|
34
|
-
@session_index = node[
|
35
|
-
@session_not_on_or_after = Time.parse(node[
|
36
|
-
@authn_context_class_ref = node.at_xpath(
|
43
|
+
@authn_instant = Time.parse(node["AuthnInstant"])
|
44
|
+
@session_index = node["SessionIndex"]
|
45
|
+
@session_not_on_or_after = Time.parse(node["SessionNotOnOrAfter"]) if node["SessionNotOnOrAfter"]
|
46
|
+
@authn_context_class_ref = node.at_xpath("saml:AuthnContext/saml:AuthnContextClassRef",
|
47
|
+
Namespaces::ALL)&.content&.strip
|
37
48
|
end
|
38
49
|
|
39
50
|
# (see Base#build)
|
40
51
|
def build(builder)
|
41
|
-
builder[
|
42
|
-
authn_statement.parent[
|
43
|
-
authn_statement.parent[
|
44
|
-
authn_statement[
|
45
|
-
authn_context[
|
52
|
+
builder["saml"].AuthnStatement("AuthnInstant" => authn_instant.iso8601) do |authn_statement|
|
53
|
+
authn_statement.parent["SessionIndex"] = session_index if session_index
|
54
|
+
authn_statement.parent["SessionNotOnOrAfter"] = session_not_on_or_after.iso8601 if session_not_on_or_after
|
55
|
+
authn_statement["saml"].AuthnContext do |authn_context|
|
56
|
+
authn_context["saml"].AuthnContextClassRef(authn_context_class_ref) if authn_context_class_ref
|
46
57
|
end
|
47
58
|
end
|
48
59
|
end
|
data/lib/saml2/base.rb
CHANGED
@@ -1,19 +1,61 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "saml2/namespaces"
|
4
4
|
|
5
5
|
module SAML2
|
6
6
|
# @abstract
|
7
7
|
class Base
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
8
|
+
class << self
|
9
|
+
def lookup_qname(qname, namespaces)
|
10
|
+
prefix, local_name = split_qname(qname)
|
11
|
+
[lookup_namespace(prefix, namespaces), local_name]
|
12
|
+
end
|
13
|
+
|
14
|
+
# Create an appropriate object to represent the given XML element.
|
15
|
+
#
|
16
|
+
# @param node [Nokogiri::XML::Element, nil]
|
17
|
+
# @return [Base, nil]
|
18
|
+
def from_xml(node)
|
19
|
+
return nil unless node
|
20
|
+
|
21
|
+
result = new
|
22
|
+
result.from_xml(node)
|
23
|
+
result
|
24
|
+
end
|
25
|
+
|
26
|
+
def load_string_array(node, element)
|
27
|
+
node.xpath(element, Namespaces::ALL).map do |element_node|
|
28
|
+
element_node.content&.strip
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def load_object_array(node, element, klass = nil)
|
33
|
+
node.xpath(element, Namespaces::ALL).map do |element_node|
|
34
|
+
if klass.nil?
|
35
|
+
SAML2.const_get(element_node.name, false).from_xml(element_node)
|
36
|
+
elsif klass.is_a?(Hash)
|
37
|
+
klass[element_node.name].from_xml(element_node)
|
38
|
+
else
|
39
|
+
klass.from_xml(element_node)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def split_qname(qname)
|
47
|
+
if qname.include?(":")
|
48
|
+
qname.split(":", 2)
|
49
|
+
else
|
50
|
+
[nil, qname]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def lookup_namespace(prefix, namespaces)
|
55
|
+
return nil if namespaces.empty?
|
56
|
+
|
57
|
+
namespaces[prefix.empty? ? "xmlns" : "xmlns:#{prefix}"]
|
58
|
+
end
|
17
59
|
end
|
18
60
|
|
19
61
|
# @return [Nokogiri::XML::Element]
|
@@ -45,13 +87,15 @@ module SAML2
|
|
45
87
|
if pretty
|
46
88
|
xml.to_s
|
47
89
|
else
|
48
|
-
xml.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML |
|
90
|
+
xml.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML |
|
91
|
+
Nokogiri::XML::Node::SaveOptions::NO_DECLARATION)
|
49
92
|
end
|
50
93
|
elsif pretty
|
51
94
|
to_xml.to_s
|
52
95
|
else
|
53
96
|
# make sure to not FORMAT it - it breaks signatures!
|
54
|
-
to_xml.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML |
|
97
|
+
to_xml.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML |
|
98
|
+
Nokogiri::XML::Node::SaveOptions::NO_DECLARATION)
|
55
99
|
end
|
56
100
|
end
|
57
101
|
|
@@ -62,7 +106,11 @@ module SAML2
|
|
62
106
|
# not be created until their attribute is accessed.
|
63
107
|
# @return [String]
|
64
108
|
def inspect
|
65
|
-
"#<#{self.class.name} #{instance_variables.
|
109
|
+
"#<#{self.class.name} #{instance_variables.filter_map do |iv|
|
110
|
+
next if iv == :@xml
|
111
|
+
|
112
|
+
"#{iv}=#{instance_variable_get(iv).inspect}"
|
113
|
+
end.join(", ")}>"
|
66
114
|
end
|
67
115
|
|
68
116
|
# Serialize this object to XML
|
@@ -84,8 +132,7 @@ module SAML2
|
|
84
132
|
#
|
85
133
|
# @param builder [Nokogiri::XML::Builder] The builder helper object to serialize to.
|
86
134
|
# @return [void]
|
87
|
-
def build(builder)
|
88
|
-
end
|
135
|
+
def build(builder); end
|
89
136
|
|
90
137
|
# Decrypt (in-place) encrypted portions of this object
|
91
138
|
#
|
@@ -107,13 +154,14 @@ module SAML2
|
|
107
154
|
encrypted_nodes.each do |node|
|
108
155
|
this_nodes_keys = keys
|
109
156
|
if keys.nil?
|
110
|
-
allowed_certs = node.xpath("dsig:KeyInfo/xenc:EncryptedKey/dsig:KeyInfo/dsig:X509Data",
|
157
|
+
allowed_certs = node.xpath("dsig:KeyInfo/xenc:EncryptedKey/dsig:KeyInfo/dsig:X509Data",
|
158
|
+
SAML2::Namespaces::ALL).map do |x509data|
|
111
159
|
if (cert = x509data.at_xpath("dsig:X509Certificate", SAML2::Namespaces::ALL)&.content&.strip)
|
112
160
|
OpenSSL::X509::Certificate.new(Base64.decode64(cert))
|
113
161
|
elsif (issuer_serial = x509data.at_xpath("dsig:X509IssuerSerial", SAML2::Namespaces::ALL))
|
114
162
|
{
|
115
|
-
|
116
|
-
|
163
|
+
issuer: issuer_serial.at_xpath("dsig:X509IssuerName", SAML2::Namespaces::ALL).content.strip,
|
164
|
+
serial: issuer_serial.at_xpath("dsig:X509SerialNumber", SAML2::Namespaces::ALL).content.strip.to_i
|
117
165
|
}
|
118
166
|
elsif (subject_name = x509data.at_xpath("dsig:X509SubjectName", SAML2::Namespaces::ALL)&.content&.strip)
|
119
167
|
subject_name
|
@@ -126,42 +174,16 @@ module SAML2
|
|
126
174
|
|
127
175
|
old_node = node.parent
|
128
176
|
this_nodes_keys.each_with_index do |key, i|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
raise if i - 1 == this_nodes_keys.length
|
134
|
-
end
|
177
|
+
old_node.replace(node.decrypt_with(key: key))
|
178
|
+
rescue XMLSec::DecryptionError
|
179
|
+
# swallow errors on all but the last key
|
180
|
+
raise if i - 1 == this_nodes_keys.length
|
135
181
|
end
|
136
182
|
end
|
137
183
|
!encrypted_nodes.empty?
|
138
184
|
end
|
139
185
|
|
140
|
-
|
141
|
-
node.xpath(element, Namespaces::ALL).map do |element_node|
|
142
|
-
element_node.content&.strip
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
|
147
|
-
def self.load_object_array(node, element, klass = nil)
|
148
|
-
node.xpath(element, Namespaces::ALL).map do |element_node|
|
149
|
-
if klass.nil?
|
150
|
-
SAML2.const_get(element_node.name, false).from_xml(element_node)
|
151
|
-
elsif klass.is_a?(Hash)
|
152
|
-
klass[element_node.name].from_xml(element_node)
|
153
|
-
else
|
154
|
-
klass.from_xml(element_node)
|
155
|
-
end
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
def self.lookup_qname(qname, namespaces)
|
160
|
-
prefix, local_name = split_qname(qname)
|
161
|
-
[lookup_namespace(prefix, namespaces), local_name]
|
162
|
-
end
|
163
|
-
|
164
|
-
protected
|
186
|
+
private
|
165
187
|
|
166
188
|
def load_string_array(node, element)
|
167
189
|
self.class.load_string_array(node, element)
|
@@ -172,20 +194,7 @@ module SAML2
|
|
172
194
|
end
|
173
195
|
|
174
196
|
def encrypted_nodes
|
175
|
-
xml.xpath(
|
176
|
-
end
|
177
|
-
|
178
|
-
def self.split_qname(qname)
|
179
|
-
if qname.include?(':')
|
180
|
-
qname.split(':', 2)
|
181
|
-
else
|
182
|
-
[nil, qname]
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
def self.lookup_namespace(prefix, namespaces)
|
187
|
-
return nil if namespaces.empty?
|
188
|
-
namespaces[prefix.empty? ? 'xmlns' : "xmlns:#{prefix}"]
|
197
|
+
xml.xpath("//xenc:EncryptedData", Namespaces::ALL)
|
189
198
|
end
|
190
199
|
end
|
191
200
|
end
|
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "base64"
|
4
4
|
|
5
5
|
module SAML2
|
6
6
|
module Bindings
|
7
|
-
module HTTP_POST
|
8
|
-
URN ="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
7
|
+
module HTTP_POST # rubocop:disable Naming/ClassAndModuleCamelCase
|
8
|
+
URN = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
9
9
|
|
10
10
|
class << self
|
11
11
|
# Decode and parse a Base64 encoded SAML message.
|
@@ -16,7 +16,7 @@ module SAML2
|
|
16
16
|
# @return [[Message, String]]
|
17
17
|
# The Message and the RelayState.
|
18
18
|
def decode(post_params)
|
19
|
-
base64 = post_params[
|
19
|
+
base64 = post_params["SAMLRequest"] || post_params["SAMLResponse"]
|
20
20
|
raise MissingMessage unless base64
|
21
21
|
|
22
22
|
raise MessageTooLarge if base64.bytesize > SAML2.config[:max_message_size]
|
@@ -28,7 +28,7 @@ module SAML2
|
|
28
28
|
end
|
29
29
|
|
30
30
|
message = Message.parse(xml)
|
31
|
-
[message, post_params[
|
31
|
+
[message, post_params["RelayState"]]
|
32
32
|
end
|
33
33
|
|
34
34
|
# Encode a SAML message into Base64 POST params.
|
@@ -40,9 +40,9 @@ module SAML2
|
|
40
40
|
# +SAMLResponse+ chosen appropriately.
|
41
41
|
def encode(message, relay_state: nil)
|
42
42
|
xml = message.to_s(pretty: false)
|
43
|
-
key = message.is_a?(Request) ?
|
43
|
+
key = message.is_a?(Request) ? "SAMLRequest" : "SAMLResponse"
|
44
44
|
post_params = { key => Base64.encode64(xml) }
|
45
|
-
post_params[
|
45
|
+
post_params["RelayState"] = relay_state if relay_state
|
46
46
|
post_params
|
47
47
|
end
|
48
48
|
end
|
@@ -1,16 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
3
|
+
require "base64"
|
4
|
+
require "uri"
|
5
|
+
require "zlib"
|
6
6
|
|
7
|
-
require
|
8
|
-
require
|
7
|
+
require "saml2/bindings"
|
8
|
+
require "saml2/message"
|
9
9
|
|
10
10
|
module SAML2
|
11
11
|
module Bindings
|
12
12
|
module HTTPRedirect
|
13
|
-
URN ="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
13
|
+
URN = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
14
14
|
|
15
15
|
module SigAlgs
|
16
16
|
DSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#dsa-sha1"
|
@@ -51,18 +51,19 @@ module SAML2
|
|
51
51
|
end
|
52
52
|
|
53
53
|
raise MissingMessage unless uri.query
|
54
|
+
|
54
55
|
query = URI.decode_www_form(uri.query)
|
55
|
-
base64 = query.assoc(
|
56
|
+
base64 = query.assoc("SAMLRequest")&.last
|
56
57
|
if base64
|
57
|
-
message_param =
|
58
|
+
message_param = "SAMLRequest"
|
58
59
|
else
|
59
|
-
base64 = query.assoc(
|
60
|
-
message_param =
|
60
|
+
base64 = query.assoc("SAMLResponse")&.last
|
61
|
+
message_param = "SAMLResponse"
|
61
62
|
end
|
62
|
-
encoding = query.assoc(
|
63
|
-
relay_state = query.assoc(
|
64
|
-
signature = query.assoc(
|
65
|
-
sig_alg = query.assoc(
|
63
|
+
encoding = query.assoc("SAMLEncoding")&.last
|
64
|
+
relay_state = query.assoc("RelayState")&.last
|
65
|
+
signature = query.assoc("Signature")&.last
|
66
|
+
sig_alg = query.assoc("SigAlg")&.last
|
66
67
|
raise MissingMessage unless base64
|
67
68
|
|
68
69
|
raise UnsupportedEncoding if encoding && encoding != Encodings::DEFLATE
|
@@ -76,7 +77,7 @@ module SAML2
|
|
76
77
|
end
|
77
78
|
|
78
79
|
zstream = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
79
|
-
xml =
|
80
|
+
xml = +""
|
80
81
|
begin
|
81
82
|
# do it in 1K slices, so we can protect against bombs
|
82
83
|
(0..deflated.bytesize / 1024).each do |i|
|
@@ -106,19 +107,19 @@ module SAML2
|
|
106
107
|
end
|
107
108
|
|
108
109
|
base_string = find_raw_query_param(uri.query, message_param)
|
109
|
-
base_string <<
|
110
|
-
base_string <<
|
110
|
+
base_string << "&" << find_raw_query_param(uri.query, "RelayState") if relay_state
|
111
|
+
base_string << "&" << find_raw_query_param(uri.query, "SigAlg")
|
111
112
|
|
112
113
|
valid_signature = false
|
113
114
|
# there could be multiple certificates to try
|
114
115
|
Array(public_key).each do |key|
|
115
|
-
hash = (sig_alg == SigAlgs::RSA_SHA256 ? OpenSSL::Digest::SHA256 : OpenSSL::Digest::SHA1)
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
116
|
+
hash = ((sig_alg == SigAlgs::RSA_SHA256) ? OpenSSL::Digest::SHA256 : OpenSSL::Digest::SHA1)
|
117
|
+
next unless key.verify(hash.new, signature, base_string)
|
118
|
+
|
119
|
+
# notify the caller which certificate was used
|
120
|
+
public_key_used&.call(key)
|
121
|
+
valid_signature = true
|
122
|
+
break
|
122
123
|
end
|
123
124
|
raise InvalidSignature unless valid_signature
|
124
125
|
end
|
@@ -145,8 +146,8 @@ module SAML2
|
|
145
146
|
original_query = URI.decode_www_form(result.query) if result.query
|
146
147
|
original_query ||= []
|
147
148
|
# remove any SAML protocol parameters
|
148
|
-
%w
|
149
|
-
original_query.delete_if { |(k,
|
149
|
+
%w[SAMLEncoding SAMLRequest SAMLResponse RelayState SigAlg Signature].each do |param|
|
150
|
+
original_query.delete_if { |(k, _v)| k == param }
|
150
151
|
end
|
151
152
|
|
152
153
|
xml = message.to_s(pretty: false)
|
@@ -156,16 +157,19 @@ module SAML2
|
|
156
157
|
base64 = Base64.strict_encode64(deflated)
|
157
158
|
|
158
159
|
query = []
|
159
|
-
query << [message.is_a?(Request) ?
|
160
|
-
query << [
|
160
|
+
query << [message.is_a?(Request) ? "SAMLRequest" : "SAMLResponse", base64]
|
161
|
+
query << ["RelayState", relay_state] if relay_state
|
161
162
|
if private_key
|
162
|
-
|
163
|
+
unless SigAlgs::RECOGNIZED.include?(sig_alg)
|
164
|
+
raise ArgumentError,
|
165
|
+
"Unsupported signature algorithm #{sig_alg}"
|
166
|
+
end
|
163
167
|
|
164
|
-
query << [
|
168
|
+
query << ["SigAlg", sig_alg]
|
165
169
|
base_string = URI.encode_www_form(query)
|
166
|
-
hash = (sig_alg == SigAlgs::RSA_SHA256 ? OpenSSL::Digest::SHA256 : OpenSSL::Digest::SHA1)
|
170
|
+
hash = ((sig_alg == SigAlgs::RSA_SHA256) ? OpenSSL::Digest::SHA256 : OpenSSL::Digest::SHA1)
|
167
171
|
signature = private_key.sign(hash.new, base_string)
|
168
|
-
query << [
|
172
|
+
query << ["Signature", Base64.strict_encode64(signature)]
|
169
173
|
end
|
170
174
|
|
171
175
|
result.query = URI.encode_www_form(original_query + query)
|
@@ -177,7 +181,7 @@ module SAML2
|
|
177
181
|
# we need to find the param, and return it still encoded from the URL
|
178
182
|
def find_raw_query_param(query, param)
|
179
183
|
start = query.index(param)
|
180
|
-
finish = (query.index(
|
184
|
+
finish = (query.index("&", start + param.length + 1) || 0) - 1
|
181
185
|
query[start..finish]
|
182
186
|
end
|
183
187
|
end
|