saml2 3.1.1 → 3.1.3
Sign up to get free protection for your applications and to get access to all the features.
- 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 -137
- data/schemas/MetadataExchange.xsd +0 -112
- data/schemas/metadata_combined.xsd +0 -13
- data/schemas/oasis-200401-wss-wssecurity-secext-1.0.xsd +0 -195
- data/schemas/oasis-200401-wss-wssecurity-utility-1.0.xsd +0 -108
- data/schemas/saml-schema-assertion-2.0.xsd +0 -283
- data/schemas/saml-schema-metadata-2.0.xsd +0 -339
- data/schemas/saml-schema-protocol-2.0.xsd +0 -302
- data/schemas/sstc-saml-metadata-ext-query.xsd +0 -66
- data/schemas/ws-addr.xsd +0 -137
- data/schemas/ws-authorization.xsd +0 -145
- data/schemas/ws-federation.xsd +0 -471
- data/schemas/ws-securitypolicy-1.2.xsd +0 -1205
- data/schemas/xenc-schema.xsd +0 -136
- data/schemas/xml.xsd +0 -287
- data/schemas/xmldsig-core-schema.xsd +0 -309
- 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
|