ruby-saml 1.3.1 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of ruby-saml might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/README.md +41 -30
- data/changelog.md +24 -0
- data/lib/onelogin/ruby-saml/authrequest.rb +5 -2
- data/lib/onelogin/ruby-saml/response.rb +171 -50
- data/lib/onelogin/ruby-saml/saml_message.rb +3 -1
- data/lib/onelogin/ruby-saml/settings.rb +4 -3
- data/lib/onelogin/ruby-saml/version.rb +1 -1
- data/lib/xml_security.rb +6 -6
- data/test/request_test.rb +7 -0
- data/test/response_test.rb +161 -14
- data/test/responses/invalids/duplicated_attributes.xml.base64 +1 -0
- data/test/responses/invalids/empty_destination.xml.base64 +1 -0
- data/test/responses/invalids/empty_nameid.xml.base64 +1 -0
- data/test/responses/invalids/encrypted_new_attack.xml.base64 +1 -0
- data/test/responses/invalids/invalid_issuer_message.xml.base64 +1 -1
- data/test/responses/invalids/no_authnstatement.xml.base64 +1 -0
- data/test/responses/invalids/no_conditions.xml.base64 +1 -0
- data/test/responses/invalids/no_issuer_assertion.xml.base64 +1 -0
- data/test/responses/invalids/no_issuer_response.xml.base64 +1 -0
- data/test/responses/invalids/no_nameid.xml.base64 +1 -0
- data/test/responses/invalids/response_with_concealed_signed_assertion.xml +51 -0
- data/test/responses/invalids/response_with_doubled_signed_assertion.xml +49 -0
- data/test/responses/invalids/wrong_spnamequalifier.xml.base64 +1 -0
- data/test/responses/response_with_multiple_attribute_statements.xml +72 -0
- data/test/responses/response_with_signed_assertion_3.xml +30 -0
- data/test/xml_security_test.rb +33 -0
- metadata +30 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 268a395d4ed90393709bec460cd818d644bd7269
|
4
|
+
data.tar.gz: 0507a8928e591569b9be618d2dac54b8c83a1847
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 59e8e5300dcd4f521c19216686753cd2ce1a2480383bf6c873601a1031d96ef59241e13076a6b2e89cee61cc43f64c2afc383d6c985a8186cee0a7efe433aabd
|
7
|
+
data.tar.gz: 00dfd3b12d124c41282b7edce5ae4214f96d696314120776f0cbeadc6c86c35cfac1279b8d3ef0b23ff6559ffec6365681254da2784d86b8fa659a825989ba02
|
data/README.md
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# Ruby SAML [![Build Status](https://secure.travis-ci.org/onelogin/ruby-saml.png)](http://travis-ci.org/onelogin/ruby-saml) [![Coverage Status](https://coveralls.io/repos/onelogin/ruby-saml/badge.svg?branch=master%0A)](https://coveralls.io/r/onelogin/ruby-saml?branch=master%0A) [![Gem Version](https://badge.fury.io/rb/ruby-saml.svg)](http://badge.fury.io/rb/ruby-saml)
|
2
2
|
|
3
|
+
## Updating from 1.3.x to 1.4.X
|
4
|
+
|
5
|
+
Version `1.4.0` is a recommended update for all Ruby SAML users as it includes security improvements.
|
6
|
+
|
3
7
|
## Updating from 1.2.x to 1.3.X
|
4
8
|
|
5
9
|
Version `1.3.0` is a recommended update for all Ruby SAML users as it includes security fixes. It adds security improvements in order to prevent Signature wrapping attacks. [CVE-2016-5697](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-5697)
|
@@ -92,7 +96,7 @@ require 'onelogin/ruby-saml/authrequest'
|
|
92
96
|
|
93
97
|
### Installation on Ruby 1.8.7
|
94
98
|
|
95
|
-
This gem
|
99
|
+
This gem uses Nokogiri as a dependency, which dropped support for Ruby 1.8.x in Nokogiri 1.6. When installing this gem on Ruby 1.8.7, you will need to make sure a version of Nokogiri prior to 1.6 is installed or specified if it hasn't been already.
|
96
100
|
|
97
101
|
Using `Gemfile`
|
98
102
|
|
@@ -121,7 +125,7 @@ OneLogin::RubySaml::Logging.logger = Logger.new(File.open('/var/log/ruby-saml.lo
|
|
121
125
|
|
122
126
|
## The Initialization Phase
|
123
127
|
|
124
|
-
This is the first request you will get from the identity provider. It will hit your application at a specific URL
|
128
|
+
This is the first request you will get from the identity provider. It will hit your application at a specific URL that you've announced as your SAML initialization point. The response to this initialization is a redirect back to the identity provider, which can look something like this (ignore the saml_settings method call for now):
|
125
129
|
|
126
130
|
```ruby
|
127
131
|
def init
|
@@ -130,7 +134,7 @@ def init
|
|
130
134
|
end
|
131
135
|
```
|
132
136
|
|
133
|
-
Once you've redirected back to the identity provider, it will ensure that the user has been authorized and redirect back to your application for final consumption
|
137
|
+
Once you've redirected back to the identity provider, it will ensure that the user has been authorized and redirect back to your application for final consumption. This can look something like this (the `authorize_success` and `authorize_failure` methods are specific to your application):
|
134
138
|
|
135
139
|
```ruby
|
136
140
|
def consume
|
@@ -147,17 +151,17 @@ def consume
|
|
147
151
|
end
|
148
152
|
```
|
149
153
|
|
150
|
-
In the above there are a few assumptions
|
151
|
-
|
152
|
-
If the assertion of the SAMLResponse is not encrypted, you can initialize the Response without the :settings parameter and set it later,
|
154
|
+
In the above there are a few assumptions, one being that `response.nameid` is an email address. This is all handled with how you specify the settings that are in play via the `saml_settings` method. That could be implemented along the lines of this:
|
153
155
|
|
154
156
|
```
|
155
157
|
response = OneLogin::RubySaml::Response.new(params[:SAMLResponse])
|
156
158
|
response.settings = saml_settings
|
157
159
|
```
|
158
|
-
|
159
|
-
|
160
|
-
If
|
160
|
+
|
161
|
+
If the assertion of the SAMLResponse is not encrypted, you can initialize the Response without the `:settings` parameter and set it later.
|
162
|
+
If the SAMLResponse contains an encrypted assertion, you need to provide the settings in the
|
163
|
+
initialize method in order to obtain the decrypted assertion, using the service provider private key in order to decrypt.
|
164
|
+
If you don't know what expect, always use the former (set the settings on initialize).
|
161
165
|
|
162
166
|
```ruby
|
163
167
|
def saml_settings
|
@@ -175,6 +179,11 @@ def saml_settings
|
|
175
179
|
|
176
180
|
# Optional for most SAML IdPs
|
177
181
|
settings.authn_context = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
|
182
|
+
# or as an array
|
183
|
+
settings.authn_context = [
|
184
|
+
"urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport",
|
185
|
+
"urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
|
186
|
+
]
|
178
187
|
|
179
188
|
# Optional bindings (defaults to Redirect for logout POST for acs)
|
180
189
|
settings.assertion_consumer_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
@@ -184,14 +193,14 @@ def saml_settings
|
|
184
193
|
end
|
185
194
|
```
|
186
195
|
|
187
|
-
Some assertion validations can be skipped by passing parameters to OneLogin::RubySaml::Response.new()
|
196
|
+
Some assertion validations can be skipped by passing parameters to `OneLogin::RubySaml::Response.new()`. For example, you can skip the `Conditions` validation or the `SubjectConfirmation` validations by initializing the response with different options:
|
188
197
|
|
189
198
|
```ruby
|
190
199
|
response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], {skip_conditions: true}) # skips conditions
|
191
200
|
response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], {skip_subject_confirmation: true}) # skips subject confirmation
|
192
201
|
```
|
193
202
|
|
194
|
-
|
203
|
+
All that's left is to wrap everything in a controller and reference it in the initialization and consumption URLs in OneLogin. A full controller example could look like this:
|
195
204
|
|
196
205
|
```ruby
|
197
206
|
# This controller expects you to use the URLs /saml/init and /saml/consume in your OneLogin application.
|
@@ -270,8 +279,10 @@ The following attributes are set:
|
|
270
279
|
* idp_slo_target_url
|
271
280
|
* idp_cert_fingerprint
|
272
281
|
|
273
|
-
|
274
|
-
|
282
|
+
## Retrieving Attributes
|
283
|
+
|
284
|
+
If you are using `saml:AttributeStatement` to transfer data like the username, you can access all the attributes through `response.attributes`. It contains all the `saml:AttributeStatement`s with its 'Name' as an indifferent key and one or more `saml:AttributeValue`s as values. The value returned depends on the value of the
|
285
|
+
`single_value_compatibility` (when activated, only the first value is returned)
|
275
286
|
|
276
287
|
```ruby
|
277
288
|
response = OneLogin::RubySaml::Response.new(params[:SAMLResponse])
|
@@ -280,7 +291,7 @@ response.settings = saml_settings
|
|
280
291
|
response.attributes[:username]
|
281
292
|
```
|
282
293
|
|
283
|
-
Imagine this saml:AttributeStatement
|
294
|
+
Imagine this `saml:AttributeStatement`
|
284
295
|
|
285
296
|
```xml
|
286
297
|
<saml:AttributeStatement>
|
@@ -380,15 +391,15 @@ pp(response.attributes.multi(:not_exists))
|
|
380
391
|
# => nil
|
381
392
|
```
|
382
393
|
|
383
|
-
The saml:AuthnContextClassRef of the AuthNRequest can be provided by `settings.authn_context
|
384
|
-
|
394
|
+
The `saml:AuthnContextClassRef` of the AuthNRequest can be provided by `settings.authn_context`; possible values are described at [SAMLAuthnCxt]. The comparison method can be set using `settings.authn_context_comparison` parameter. Possible values include: 'exact', 'better', 'maximum' and 'minimum' (default value is 'exact').
|
395
|
+
To add a `saml:AuthnContextDeclRef`, define `settings.authn_context_decl_ref`.
|
385
396
|
|
386
397
|
|
387
398
|
## Signing
|
388
399
|
|
389
|
-
The Ruby Toolkit supports 2 different kinds of signature: Embeded and
|
400
|
+
The Ruby Toolkit supports 2 different kinds of signature: Embeded and `GET` parameters
|
390
401
|
|
391
|
-
In order to be able to sign
|
402
|
+
In order to be able to sign, define the private key and the public cert of the service provider:
|
392
403
|
|
393
404
|
```ruby
|
394
405
|
settings.certificate = "CERTIFICATE TEXT WITH HEAD AND FOOT"
|
@@ -414,23 +425,23 @@ The settings related to sign are stored in the `security` attribute of the setti
|
|
414
425
|
settings.security[:embed_sign] = false
|
415
426
|
```
|
416
427
|
|
417
|
-
Notice that the RelayState parameter is used when creating the Signature on the HTTP-Redirect Binding
|
418
|
-
|
419
|
-
|
428
|
+
Notice that the RelayState parameter is used when creating the Signature on the HTTP-Redirect Binding.
|
429
|
+
Remember to provide it to the Signature builder if you are sending a `GET RelayState` parameter or the
|
430
|
+
signature validation process will fail at the Identity Provider.
|
420
431
|
|
421
432
|
The Service Provider will sign the request/responses with its private key.
|
422
433
|
The Identity Provider will validate the sign of the received request/responses with the public x500 cert of the
|
423
434
|
Service Provider.
|
424
435
|
|
425
|
-
Notice that this toolkit uses 'settings.certificate' and 'settings.private_key' for the sign and
|
436
|
+
Notice that this toolkit uses 'settings.certificate' and 'settings.private_key' for the sign and decrypt processes.
|
426
437
|
|
427
|
-
Enable/disable the soft mode
|
438
|
+
Enable/disable the soft mode with the `settings.soft` parameter. When set to `false`, saml validations errors will raise an exception.
|
428
439
|
|
429
440
|
## Decrypting
|
430
441
|
|
431
442
|
The Ruby Toolkit supports EncryptedAssertion.
|
432
443
|
|
433
|
-
In order to be able to decrypt a SAML Response that contains a EncryptedAssertion
|
444
|
+
In order to be able to decrypt a SAML Response that contains a EncryptedAssertion you need define the private key and the public cert of the service provider, then share this with the Identity Provider.
|
434
445
|
|
435
446
|
```ruby
|
436
447
|
settings.certificate = "CERTIFICATE TEXT WITH HEAD AND FOOT"
|
@@ -440,13 +451,13 @@ In order to be able to decrypt a SAML Response that contains a EncryptedAssertio
|
|
440
451
|
The Identity Provider will encrypt the Assertion with the public cert of the Service Provider.
|
441
452
|
The Service Provider will decrypt the EncryptedAssertion with its private key.
|
442
453
|
|
443
|
-
Notice that this toolkit uses 'settings.certificate' and 'settings.private_key' for the sign and
|
454
|
+
Notice that this toolkit uses 'settings.certificate' and 'settings.private_key' for the sign and decrypt processes.
|
444
455
|
|
445
456
|
## Single Log Out
|
446
457
|
|
447
458
|
The Ruby Toolkit supports SP-initiated Single Logout and IdP-Initiated Single Logout.
|
448
459
|
|
449
|
-
Here is an example that we could add to our previous controller to generate and send a SAML Logout Request to the IdP
|
460
|
+
Here is an example that we could add to our previous controller to generate and send a SAML Logout Request to the IdP:
|
450
461
|
|
451
462
|
```ruby
|
452
463
|
# Create a SP initiated SLO
|
@@ -475,7 +486,7 @@ def sp_logout_request
|
|
475
486
|
end
|
476
487
|
```
|
477
488
|
|
478
|
-
|
489
|
+
This method processes the SAML Logout Response sent by the IdP as the reply of the SAML Logout Request:
|
479
490
|
|
480
491
|
```ruby
|
481
492
|
# After sending an SP initiated LogoutRequest to the IdP, we need to accept
|
@@ -510,7 +521,7 @@ def delete_session
|
|
510
521
|
end
|
511
522
|
```
|
512
523
|
|
513
|
-
Here is an example that we could add to our previous controller to process a SAML Logout Request from the IdP and reply a SAML Logout Response to the IdP
|
524
|
+
Here is an example that we could add to our previous controller to process a SAML Logout Request from the IdP and reply with a SAML Logout Response to the IdP:
|
514
525
|
|
515
526
|
```ruby
|
516
527
|
# Method to handle IdP initiated logouts
|
@@ -576,11 +587,11 @@ end
|
|
576
587
|
|
577
588
|
## Clock Drift
|
578
589
|
|
579
|
-
Server clocks tend to drift naturally. If during validation of the response you get the error "Current time is earlier than NotBefore condition"
|
590
|
+
Server clocks tend to drift naturally. If during validation of the response you get the error "Current time is earlier than NotBefore condition", this may be due to clock differences between your system and that of the Identity Provider.
|
580
591
|
|
581
592
|
First, ensure that both systems synchronize their clocks, using for example the industry standard [Network Time Protocol (NTP)](http://en.wikipedia.org/wiki/Network_Time_Protocol).
|
582
593
|
|
583
|
-
Even then you may experience intermittent issues
|
594
|
+
Even then you may experience intermittent issues, as the clock of the Identity Provider may drift slightly ahead of your system clocks. To allow for a small amount of clock drift, you can initialize the response by passing in an option named `:allowed_clock_drift`. Its value must be given in a number (and/or fraction) of seconds. The value given is added to the current time at which the response is validated before it's tested against the `NotBefore` assertion. For example:
|
584
595
|
|
585
596
|
```ruby
|
586
597
|
response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], :allowed_clock_drift => 1.second)
|
data/changelog.md
CHANGED
@@ -1,5 +1,29 @@
|
|
1
1
|
# RubySaml Changelog
|
2
2
|
|
3
|
+
### 1.4.0 (October 13, 2016)
|
4
|
+
* Several security improvements:
|
5
|
+
* Conditions element required and unique.
|
6
|
+
* AuthnStatement element required and unique.
|
7
|
+
* SPNameQualifier must math the SP EntityID
|
8
|
+
* Reject saml:Attribute element with same “Name” attribute
|
9
|
+
* Reject empty nameID
|
10
|
+
* Require Issuer element. (Must match IdP EntityID).
|
11
|
+
* Destination value can't be blank (if present must match ACS URL).
|
12
|
+
* Check that the EncryptedAssertion element only contains 1 Assertion element.
|
13
|
+
|
14
|
+
* [#335](https://github.com/onelogin/ruby-saml/pull/335) Explicitly parse as XML and fix setting of Nokogiri options.
|
15
|
+
* [#345](https://github.com/onelogin/ruby-saml/pull/345)Support multiple settings.auth_context
|
16
|
+
* More tests to prevent XML Signature Wrapping
|
17
|
+
* [#342](https://github.com/onelogin/ruby-saml/pull/342) Correct the usage of Mutex
|
18
|
+
* [352](https://github.com/onelogin/ruby-saml/pull/352) Support multiple AttributeStatement tags
|
19
|
+
|
20
|
+
|
21
|
+
### 1.3.1 (July 10, 2016)
|
22
|
+
* Fix response_test.rb of gem 1.3.0
|
23
|
+
* Add reference to Security Guidelines
|
24
|
+
* Update License
|
25
|
+
* [#334](https://github.com/onelogin/ruby-saml/pull/334) Keep API backward-compatibility on IdpMetadataParser fingerprint method.
|
26
|
+
|
3
27
|
### 1.3.0 (June 24, 2016)
|
4
28
|
* [Security Fix](https://github.com/onelogin/ruby-saml/commit/a571f52171e6bfd87db59822d1d9e8c38fb3b995) Add extra validations to prevent Signature wrapping attacks
|
5
29
|
* Fix XMLSecurity SHA256 and SHA512 uris
|
@@ -136,8 +136,11 @@ module OneLogin
|
|
136
136
|
}
|
137
137
|
|
138
138
|
if settings.authn_context != nil
|
139
|
-
|
140
|
-
|
139
|
+
authn_contexts = settings.authn_context.is_a?(Array) ? settings.authn_context : [settings.authn_context]
|
140
|
+
authn_contexts.each do |authn_context|
|
141
|
+
class_ref = requested_context.add_element "saml:AuthnContextClassRef"
|
142
|
+
class_ref.text = authn_context
|
143
|
+
end
|
141
144
|
end
|
142
145
|
# add saml:AuthnContextDeclRef element
|
143
146
|
if settings.authn_context_decl_ref != nil
|
@@ -88,6 +88,23 @@ module OneLogin
|
|
88
88
|
|
89
89
|
alias_method :nameid_format, :name_id_format
|
90
90
|
|
91
|
+
# @return [String] the NameID SPNameQualifier provided by the SAML response from the IdP.
|
92
|
+
#
|
93
|
+
def name_id_spnamequalifier
|
94
|
+
@name_id_spnamequalifier ||=
|
95
|
+
if name_id_node && name_id_node.attribute("SPNameQualifier")
|
96
|
+
name_id_node.attribute("SPNameQualifier").value
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# @return [String] the NameID NameQualifier provided by the SAML response from the IdP.
|
101
|
+
#
|
102
|
+
def name_id_namequalifier
|
103
|
+
@name_id_namequalifier ||=
|
104
|
+
if name_id_node && name_id_node.attribute("NameQualifier")
|
105
|
+
name_id_node.attribute("NameQualifier").value
|
106
|
+
end
|
107
|
+
end
|
91
108
|
|
92
109
|
# Gets the SessionIndex from the AuthnStatement.
|
93
110
|
# Could be used to be stored in the local session in order
|
@@ -115,35 +132,34 @@ module OneLogin
|
|
115
132
|
# attributes['name']
|
116
133
|
#
|
117
134
|
# @return [Attributes] OneLogin::RubySaml::Attributes enumerable collection.
|
118
|
-
#
|
135
|
+
#
|
119
136
|
def attributes
|
120
137
|
@attr_statements ||= begin
|
121
138
|
attributes = Attributes.new
|
122
139
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
140
|
+
stmt_elements = xpath_from_signed_assertion('/a:AttributeStatement')
|
141
|
+
stmt_elements.each do |stmt_element|
|
142
|
+
stmt_element.elements.each do |attr_element|
|
143
|
+
name = attr_element.attributes["Name"]
|
144
|
+
values = attr_element.elements.collect{|e|
|
145
|
+
if (e.elements.nil? || e.elements.size == 0)
|
146
|
+
# SAMLCore requires that nil AttributeValues MUST contain xsi:nil XML attribute set to "true" or "1"
|
147
|
+
# otherwise the value is to be regarded as empty.
|
148
|
+
["true", "1"].include?(e.attributes['xsi:nil']) ? nil : e.text.to_s
|
149
|
+
# explicitly support saml2:NameID with saml2:NameQualifier if supplied in attributes
|
150
|
+
# this is useful for allowing eduPersonTargetedId to be passed as an opaque identifier to use to
|
151
|
+
# identify the subject in an SP rather than email or other less opaque attributes
|
152
|
+
# NameQualifier, if present is prefixed with a "/" to the value
|
153
|
+
else
|
154
|
+
REXML::XPath.match(e,'a:NameID', { "a" => ASSERTION }).collect{|n|
|
155
|
+
(n.attributes['NameQualifier'] ? n.attributes['NameQualifier'] +"/" : '') + n.text.to_s
|
156
|
+
}
|
157
|
+
end
|
158
|
+
}
|
159
|
+
|
160
|
+
attributes.add(name, values.flatten)
|
161
|
+
end
|
145
162
|
end
|
146
|
-
|
147
163
|
attributes
|
148
164
|
end
|
149
165
|
end
|
@@ -170,12 +186,15 @@ module OneLogin
|
|
170
186
|
#
|
171
187
|
def status_code
|
172
188
|
@status_code ||= begin
|
173
|
-
|
189
|
+
nodes = REXML::XPath.match(
|
174
190
|
document,
|
175
191
|
"/p:Response/p:Status/p:StatusCode",
|
176
192
|
{ "p" => PROTOCOL }
|
177
193
|
)
|
178
|
-
|
194
|
+
if nodes.size == 1
|
195
|
+
node = nodes[0]
|
196
|
+
node.attributes["Value"] if node && node.attributes
|
197
|
+
end
|
179
198
|
end
|
180
199
|
end
|
181
200
|
|
@@ -183,12 +202,15 @@ module OneLogin
|
|
183
202
|
#
|
184
203
|
def status_message
|
185
204
|
@status_message ||= begin
|
186
|
-
|
205
|
+
nodes = REXML::XPath.match(
|
187
206
|
document,
|
188
207
|
"/p:Response/p:Status/p:StatusMessage",
|
189
208
|
{ "p" => PROTOCOL }
|
190
209
|
)
|
191
|
-
|
210
|
+
if nodes.size == 1
|
211
|
+
node = nodes[0]
|
212
|
+
node.text if node
|
213
|
+
end
|
192
214
|
end
|
193
215
|
end
|
194
216
|
|
@@ -214,26 +236,6 @@ module OneLogin
|
|
214
236
|
@not_on_or_after ||= parse_time(conditions, "NotOnOrAfter")
|
215
237
|
end
|
216
238
|
|
217
|
-
# Gets the Issuers (from Response and Assertion).
|
218
|
-
# (returns the first node that matches the supplied xpath from the Response and from the Assertion)
|
219
|
-
# @return [Array] Array with the Issuers (REXML::Element)
|
220
|
-
#
|
221
|
-
def issuers
|
222
|
-
@issuers ||= begin
|
223
|
-
issuers = []
|
224
|
-
nodes = REXML::XPath.match(
|
225
|
-
document,
|
226
|
-
"/p:Response/a:Issuer",
|
227
|
-
{ "p" => PROTOCOL, "a" => ASSERTION }
|
228
|
-
)
|
229
|
-
nodes += xpath_from_signed_assertion("/a:Issuer")
|
230
|
-
nodes.each do |node|
|
231
|
-
issuers << node.text if node.text
|
232
|
-
end
|
233
|
-
issuers.uniq
|
234
|
-
end
|
235
|
-
end
|
236
|
-
|
237
239
|
# @return [String|nil] The InResponseTo attribute from the SAML Response.
|
238
240
|
#
|
239
241
|
def in_response_to
|
@@ -299,15 +301,19 @@ module OneLogin
|
|
299
301
|
:validate_success_status,
|
300
302
|
:validate_num_assertion,
|
301
303
|
:validate_no_encrypted_attributes,
|
304
|
+
:validate_no_duplicated_attributes,
|
302
305
|
:validate_signed_elements,
|
303
306
|
:validate_structure,
|
304
307
|
:validate_in_response_to,
|
308
|
+
:validate_one_conditions,
|
305
309
|
:validate_conditions,
|
310
|
+
:validate_one_authnstatement,
|
306
311
|
:validate_audience,
|
307
312
|
:validate_destination,
|
308
313
|
:validate_issuer,
|
309
314
|
:validate_session_expiration,
|
310
315
|
:validate_subject_confirmation,
|
316
|
+
:validate_name_id,
|
311
317
|
:validate_signature
|
312
318
|
]
|
313
319
|
|
@@ -396,6 +402,7 @@ module OneLogin
|
|
396
402
|
# @return [Boolean] True if the SAML Response contains one unique Assertion, otherwise False
|
397
403
|
#
|
398
404
|
def validate_num_assertion
|
405
|
+
error_msg = "SAML Response must contain 1 assertion"
|
399
406
|
assertions = REXML::XPath.match(
|
400
407
|
document,
|
401
408
|
"//a:Assertion",
|
@@ -408,7 +415,18 @@ module OneLogin
|
|
408
415
|
)
|
409
416
|
|
410
417
|
unless assertions.size + encrypted_assertions.size == 1
|
411
|
-
return append_error(
|
418
|
+
return append_error(error_msg)
|
419
|
+
end
|
420
|
+
|
421
|
+
unless decrypted_document.nil?
|
422
|
+
assertions = REXML::XPath.match(
|
423
|
+
decrypted_document,
|
424
|
+
"//a:Assertion",
|
425
|
+
{ "a" => ASSERTION }
|
426
|
+
)
|
427
|
+
unless assertions.size == 1
|
428
|
+
return append_error(error_msg)
|
429
|
+
end
|
412
430
|
end
|
413
431
|
|
414
432
|
true
|
@@ -428,6 +446,28 @@ module OneLogin
|
|
428
446
|
true
|
429
447
|
end
|
430
448
|
|
449
|
+
# Validates that there are not duplicated attributes
|
450
|
+
# If fails, the error is added to the errors array
|
451
|
+
# @return [Boolean] True if there are no duplicated attribute elements, otherwise False if soft=True
|
452
|
+
# @raise [ValidationError] if soft == false and validation fails
|
453
|
+
#
|
454
|
+
def validate_no_duplicated_attributes
|
455
|
+
if options[:check_duplicated_attributes]
|
456
|
+
processed_names = []
|
457
|
+
stmt_elements = xpath_from_signed_assertion('/a:AttributeStatement')
|
458
|
+
stmt_elements.each do |stmt_element|
|
459
|
+
stmt_element.elements.each do |attr_element|
|
460
|
+
name = attr_element.attributes["Name"]
|
461
|
+
if attributes.include?(name)
|
462
|
+
return append_error("Found an Attribute element with duplicated Name")
|
463
|
+
end
|
464
|
+
processed_names.add(name)
|
465
|
+
end
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
true
|
470
|
+
end
|
431
471
|
|
432
472
|
# Validates the Signed elements
|
433
473
|
# If fails, the error is added to the errors array
|
@@ -527,7 +567,14 @@ module OneLogin
|
|
527
567
|
# @return [Boolean] True if there is a Destination element that matches the Consumer Service URL, otherwise False
|
528
568
|
#
|
529
569
|
def validate_destination
|
530
|
-
return true if destination.nil?
|
570
|
+
return true if destination.nil?
|
571
|
+
|
572
|
+
if destination.empty?
|
573
|
+
error_msg = "The response has an empty Destination value"
|
574
|
+
return append_error(error_msg)
|
575
|
+
end
|
576
|
+
|
577
|
+
return true if settings.assertion_consumer_service_url.nil? || settings.assertion_consumer_service_url.empty?
|
531
578
|
|
532
579
|
unless destination == settings.assertion_consumer_service_url
|
533
580
|
error_msg = "The response was received at #{destination} instead of #{settings.assertion_consumer_service_url}"
|
@@ -537,6 +584,34 @@ module OneLogin
|
|
537
584
|
true
|
538
585
|
end
|
539
586
|
|
587
|
+
# Checks that the samlp:Response/saml:Assertion/saml:Conditions element exists and is unique.
|
588
|
+
# If fails, the error is added to the errors array
|
589
|
+
# @return [Boolean] True if there is a conditions element and is unique
|
590
|
+
#
|
591
|
+
def validate_one_conditions
|
592
|
+
conditions_nodes = xpath_from_signed_assertion('/a:Conditions')
|
593
|
+
unless conditions_nodes.size == 1
|
594
|
+
error_msg = "The Assertion must include one Conditions element"
|
595
|
+
return append_error(error_msg)
|
596
|
+
end
|
597
|
+
|
598
|
+
true
|
599
|
+
end
|
600
|
+
|
601
|
+
# Checks that the samlp:Response/saml:Assertion/saml:AuthnStatement element exists and is unique.
|
602
|
+
# If fails, the error is added to the errors array
|
603
|
+
# @return [Boolean] True if there is a authnstatement element and is unique
|
604
|
+
#
|
605
|
+
def validate_one_authnstatement
|
606
|
+
authnstatement_nodes = xpath_from_signed_assertion('/a:AuthnStatement')
|
607
|
+
unless authnstatement_nodes.size == 1
|
608
|
+
error_msg = "The Assertion must include one AuthnStatement element"
|
609
|
+
return append_error(error_msg)
|
610
|
+
end
|
611
|
+
|
612
|
+
true
|
613
|
+
end
|
614
|
+
|
540
615
|
# Validates the Conditions. (If the response was initialized with the :skip_conditions option, this validation is skipped,
|
541
616
|
# If the response was initialized with the :allowed_clock_drift option, the timing validations are relaxed by the allowed_clock_drift value)
|
542
617
|
# @return [Boolean] True if satisfies the conditions, otherwise False if soft=True
|
@@ -569,6 +644,31 @@ module OneLogin
|
|
569
644
|
def validate_issuer
|
570
645
|
return true if settings.idp_entity_id.nil?
|
571
646
|
|
647
|
+
issuers = []
|
648
|
+
issuer_response_nodes = REXML::XPath.match(
|
649
|
+
document,
|
650
|
+
"/p:Response/a:Issuer",
|
651
|
+
{ "p" => PROTOCOL, "a" => ASSERTION }
|
652
|
+
)
|
653
|
+
|
654
|
+
unless issuer_response_nodes.size == 1
|
655
|
+
error_msg = "Issuer of the Response not found or multiple."
|
656
|
+
return append_error(error_msg)
|
657
|
+
end
|
658
|
+
|
659
|
+
doc = decrypted_document.nil? ? document : decrypted_document
|
660
|
+
issuer_assertion_nodes = xpath_from_signed_assertion("/a:Issuer")
|
661
|
+
unless issuer_assertion_nodes.size == 1
|
662
|
+
error_msg = "Issuer of the Assertion not found or multiple."
|
663
|
+
return append_error(error_msg)
|
664
|
+
end
|
665
|
+
|
666
|
+
nodes = issuer_response_nodes + issuer_assertion_nodes
|
667
|
+
nodes.each do |node|
|
668
|
+
issuers << node.text if node.text
|
669
|
+
end
|
670
|
+
issuers.uniq
|
671
|
+
|
572
672
|
issuers.each do |issuer|
|
573
673
|
unless URI.parse(issuer) == URI.parse(settings.idp_entity_id)
|
574
674
|
error_msg = "Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>"
|
@@ -642,6 +742,27 @@ module OneLogin
|
|
642
742
|
true
|
643
743
|
end
|
644
744
|
|
745
|
+
# Validates the NameID element
|
746
|
+
def validate_name_id
|
747
|
+
if name_id_node.nil?
|
748
|
+
if settings.security[:want_name_id]
|
749
|
+
return append_error("No NameID element found in the assertion of the Response")
|
750
|
+
end
|
751
|
+
else
|
752
|
+
if name_id.nil? || name_id.empty?
|
753
|
+
return append_error("An empty NameID value found")
|
754
|
+
end
|
755
|
+
|
756
|
+
unless settings.issuer.nil? || settings.issuer.empty? || name_id_spnamequalifier.nil? || name_id_spnamequalifier.empty?
|
757
|
+
if name_id_spnamequalifier != settings.issuer
|
758
|
+
return append_error("The SPNameQualifier value mistmatch the SP entityID value.")
|
759
|
+
end
|
760
|
+
end
|
761
|
+
end
|
762
|
+
|
763
|
+
true
|
764
|
+
end
|
765
|
+
|
645
766
|
# Validates the Signature
|
646
767
|
# @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True
|
647
768
|
# @raise [ValidationError] if soft == false and validation fails
|