Skip to content

Commit 7081338

Browse files
authored
Merge pull request SAML-Toolkits#397 from geneshuman/master
added SubjectConfirmation Recipient validation
2 parents a3bfd52 + 3cc9b61 commit 7081338

File tree

4 files changed

+56
-27
lines changed

4 files changed

+56
-27
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,11 +193,12 @@ def saml_settings
193193
end
194194
```
195195
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:
196+
Some assertion validations can be skipped by passing parameters to `OneLogin::RubySaml::Response.new()`. For example, you can skip the `Conditions`, `Recipient`, or the `SubjectConfirmation` validations by initializing the response with different options:
197197
198198
```ruby
199199
response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], {skip_conditions: true}) # skips conditions
200200
response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], {skip_subject_confirmation: true}) # skips subject confirmation
201+
response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], {skip_recipient_check: true}) # doens't skip subject confirmation, but skips the recipient check which is a sub check of the subject_confirmation check
201202
```
202203
203204
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:

lib/onelogin/ruby-saml/response.rb

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class Response < SamlMessage
3232

3333
# Constructs the SAML Response. A Response Object that is an extension of the SamlMessage class.
3434
# @param response [String] A UUEncoded SAML response from the IdP.
35-
# @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object
35+
# @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object
3636
# Or some options for the response validation process like skip the conditions validation
3737
# with the :skip_conditions, or allow a clock_drift when checking dates with :allowed_clock_drift
3838
# or :matches_request_id that will validate that the response matches the ID of the request,
@@ -41,6 +41,7 @@ def initialize(response, options = {})
4141
raise ArgumentError.new("Response cannot be nil") if response.nil?
4242

4343
@errors = []
44+
4445
@options = options
4546
@soft = true
4647
unless options[:settings].nil?
@@ -189,7 +190,7 @@ def session_expires_at
189190

190191
# Checks if the Status has the "Success" code
191192
# @return [Boolean] True if the StatusCode is Sucess
192-
#
193+
#
193194
def success?
194195
status_code == "urn:oasis:names:tc:SAML:2.0:status:Success"
195196
end
@@ -376,15 +377,15 @@ def validate(collect_errors = false)
376377
#
377378
def validate_success_status
378379
return true if success?
379-
380+
380381
error_msg = 'The status code of the Response was not Success'
381382
status_error_msg = OneLogin::RubySaml::Utils.status_error_msg(error_msg, status_code, status_message)
382383
append_error(status_error_msg)
383384
end
384385

385386
# Validates the SAML Response against the specified schema.
386387
# @return [Boolean] True if the XML is valid, otherwise False if soft=True
387-
# @raise [ValidationError] if soft == false and validation fails
388+
# @raise [ValidationError] if soft == false and validation fails
388389
#
389390
def validate_structure
390391
structure_error_msg = "Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd"
@@ -417,7 +418,7 @@ def validate_response_state
417418
true
418419
end
419420

420-
# Validates that the SAML Response contains an ID
421+
# Validates that the SAML Response contains an ID
421422
# If fails, the error is added to the errors array.
422423
# @return [Boolean] True if the SAML Response contains an ID, otherwise returns False
423424
#
@@ -706,8 +707,9 @@ def validate_session_expiration(soft = true)
706707
end
707708

708709
# Validates if exists valid SubjectConfirmation (If the response was initialized with the :allowed_clock_drift option,
709-
# timimg validation are relaxed by the allowed_clock_drift value. If the response was initialized with the
710+
# timimg validation are relaxed by the allowed_clock_drift value. If the response was initialized with the
710711
# :skip_subject_confirmation option, this validation is skipped)
712+
# There is also an optional Recipient check
711713
# If fails, the error is added to the errors array
712714
# @return [Boolean] True if exists a valid SubjectConfirmation, otherwise False if soft=True
713715
# @raise [ValidationError] if soft == false and validation fails
@@ -717,7 +719,7 @@ def validate_subject_confirmation
717719
valid_subject_confirmation = false
718720

719721
subject_confirmation_nodes = xpath_from_signed_assertion('/a:Subject/a:SubjectConfirmation')
720-
722+
721723
now = Time.now.utc
722724
subject_confirmation_nodes.each do |subject_confirmation|
723725
if subject_confirmation.attributes.include? "Method" and subject_confirmation.attributes['Method'] != 'urn:oasis:names:tc:SAML:2.0:cm:bearer'
@@ -735,8 +737,9 @@ def validate_subject_confirmation
735737
attrs = confirmation_data_node.attributes
736738
next if (attrs.include? "InResponseTo" and attrs['InResponseTo'] != in_response_to) ||
737739
(attrs.include? "NotOnOrAfter" and (parse_time(confirmation_data_node, "NotOnOrAfter") + allowed_clock_drift) <= now) ||
738-
(attrs.include? "NotBefore" and parse_time(confirmation_data_node, "NotBefore") > (now + allowed_clock_drift))
739-
740+
(attrs.include? "NotBefore" and parse_time(confirmation_data_node, "NotBefore") > (now + allowed_clock_drift)) ||
741+
(attrs.include? "Recipient" and !options[:skip_recipient_check] and settings and attrs['Recipient'] != settings.assertion_consumer_service_url)
742+
740743
valid_subject_confirmation = true
741744
break
742745
end
@@ -808,7 +811,7 @@ def validate_signature
808811
opts[:cert] = settings.get_idp_cert
809812
fingerprint = settings.get_fingerprint
810813

811-
unless fingerprint && doc.validate_document(fingerprint, @soft, opts)
814+
unless fingerprint && doc.validate_document(fingerprint, @soft, opts)
812815
return append_error(error_msg)
813816
end
814817

lib/onelogin/ruby-saml/settings.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def single_logout_service_binding
9494
end
9595

9696
# Setter for Single Logout Service Binding.
97-
#
97+
#
9898
# (Currently we only support "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect")
9999
# @param url [String]
100100
#

test/response_test.rb

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class RubySamlTest < Minitest::Test
1717
let(:response_wrapped) { OneLogin::RubySaml::Response.new(response_document_wrapped) }
1818
let(:response_multiple_attr_values) { OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values)) }
1919
let(:response_valid_signed) { OneLogin::RubySaml::Response.new(response_document_valid_signed) }
20+
let(:response_valid_signed_without_recipient) { OneLogin::RubySaml::Response.new(response_document_valid_signed, {:skip_recipient_check => true })}
2021
let(:response_valid_signed_without_x509certificate) { OneLogin::RubySaml::Response.new(response_document_valid_signed_without_x509certificate) }
2122
let(:response_no_id) { OneLogin::RubySaml::Response.new(read_invalid_response("no_id.xml.base64")) }
2223
let(:response_no_version) { OneLogin::RubySaml::Response.new(read_invalid_response("no_saml2.xml.base64")) }
@@ -241,19 +242,19 @@ class RubySamlTest < Minitest::Test
241242
end
242243

243244
it "return true when the response is initialized with valid data" do
244-
response_valid_signed.stubs(:conditions).returns(nil)
245-
response_valid_signed.settings = settings
246-
response_valid_signed.settings.idp_cert_fingerprint = ruby_saml_cert_fingerprint
247-
assert response_valid_signed.is_valid?
248-
assert_empty response_valid_signed.errors
245+
response_valid_signed_without_recipient.stubs(:conditions).returns(nil)
246+
response_valid_signed_without_recipient.settings = settings
247+
response_valid_signed_without_recipient.settings.idp_cert_fingerprint = ruby_saml_cert_fingerprint
248+
assert response_valid_signed_without_recipient.is_valid?
249+
assert_empty response_valid_signed_without_recipient.errors
249250
end
250251

251252
it "return true when the response is initialized with valid data and using certificate instead of fingerprint" do
252-
response_valid_signed.stubs(:conditions).returns(nil)
253-
response_valid_signed.settings = settings
254-
response_valid_signed.settings.idp_cert = ruby_saml_cert_text
255-
assert response_valid_signed.is_valid?
256-
assert_empty response_valid_signed.errors
253+
response_valid_signed_without_recipient.stubs(:conditions).returns(nil)
254+
response_valid_signed_without_recipient.settings = settings
255+
response_valid_signed_without_recipient.settings.idp_cert = ruby_saml_cert_text
256+
assert response_valid_signed_without_recipient.is_valid?
257+
assert_empty response_valid_signed_without_recipient.errors
257258
end
258259

259260
it "return false when response is initialized with blank data" do
@@ -282,11 +283,11 @@ class RubySamlTest < Minitest::Test
282283
end
283284

284285
it "should be idempotent when the response is initialized with valid data" do
285-
response_valid_signed.stubs(:conditions).returns(nil)
286-
response_valid_signed.settings = settings
287-
response_valid_signed.settings.idp_cert_fingerprint = ruby_saml_cert_fingerprint
288-
assert response_valid_signed.is_valid?
289-
assert response_valid_signed.is_valid?
286+
response_valid_signed_without_recipient.stubs(:conditions).returns(nil)
287+
response_valid_signed_without_recipient.settings = settings
288+
response_valid_signed_without_recipient.settings.idp_cert_fingerprint = ruby_saml_cert_fingerprint
289+
assert response_valid_signed_without_recipient.is_valid?
290+
assert response_valid_signed_without_recipient.is_valid?
290291
end
291292

292293
it "not allow signature wrapping attack" do
@@ -382,6 +383,7 @@ class RubySamlTest < Minitest::Test
382383

383384
it "return true when a nil URI is given in the ds:Reference" do
384385
settings.idp_cert = ruby_saml_cert_text
386+
settings.assertion_consumer_service_url = "http://localhost:9001/v1/users/authorize/saml"
385387
response_without_reference_uri.settings = settings
386388
response_without_reference_uri.stubs(:conditions).returns(nil)
387389
response_without_reference_uri.is_valid?
@@ -676,6 +678,28 @@ class RubySamlTest < Minitest::Test
676678
assert_includes response_invalid_subjectconfirmation_noa.errors, "A valid SubjectConfirmation was not found on this Response"
677679
end
678680

681+
it "return true when valid subject confirmation recipient" do
682+
response_valid_signed.settings = settings
683+
response_valid_signed.settings.assertion_consumer_service_url = 'recipient'
684+
assert response_valid_signed.send(:validate_subject_confirmation)
685+
assert_empty response_valid_signed.errors
686+
assert_empty response_valid_signed.errors
687+
end
688+
689+
it "return false when invalid subject confirmation recipient" do
690+
response_valid_signed.settings = settings
691+
response_valid_signed.settings.assertion_consumer_service_url = 'not-the-recipient'
692+
assert !response_valid_signed.send(:validate_subject_confirmation)
693+
assert_includes response_valid_signed.errors, "A valid SubjectConfirmation was not found on this Response"
694+
end
695+
696+
it "return false when invalid subject confirmation recipient, but skipping the check(default)" do
697+
response_valid_signed_without_recipient.settings = settings
698+
response_valid_signed_without_recipient.settings.assertion_consumer_service_url = 'not-the-recipient'
699+
assert response_valid_signed_without_recipient.send(:validate_subject_confirmation)
700+
assert_empty response_valid_signed_without_recipient.errors
701+
end
702+
679703
it "return true when the skip_subject_confirmation option is passed and the subject confirmation is valid" do
680704
opts = {}
681705
opts[:skip_subject_confirmation] = true
@@ -1123,6 +1147,7 @@ class RubySamlTest < Minitest::Test
11231147
document.sign_document(private_key, cert)
11241148

11251149
signed_response = OneLogin::RubySaml::Response.new(document.to_s)
1150+
settings.assertion_consumer_service_url = "http://recipient"
11261151
settings.idp_cert = ruby_saml_cert_text
11271152
signed_response.settings = settings
11281153
Timecop.freeze(Time.parse("2015-03-18T04:50:24Z")) do

0 commit comments

Comments
 (0)