@@ -17,6 +17,10 @@ class Response < SamlMessage
17
17
PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
18
18
DSIG = "http://www.w3.org/2000/09/xmldsig#"
19
19
XENC = "http://www.w3.org/2001/04/xmlenc#"
20
+ SAML_NAMESPACES = {
21
+ "p" => PROTOCOL ,
22
+ "a" => ASSERTION
23
+ } . freeze
20
24
21
25
# TODO: Settings should probably be initialized too... WDYT?
22
26
@@ -303,7 +307,7 @@ def issuers
303
307
issuer_response_nodes = REXML ::XPath . match (
304
308
document ,
305
309
"/p:Response/a:Issuer" ,
306
- { "p" => PROTOCOL , "a" => ASSERTION }
310
+ SAML_NAMESPACES
307
311
)
308
312
309
313
unless issuer_response_nodes . size == 1
@@ -370,7 +374,7 @@ def assertion_encrypted?
370
374
! REXML ::XPath . first (
371
375
document ,
372
376
"(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)" ,
373
- { "p" => PROTOCOL , "a" => ASSERTION }
377
+ SAML_NAMESPACES
374
378
) . nil?
375
379
end
376
380
@@ -401,9 +405,9 @@ def validate(collect_errors = false)
401
405
:validate_id ,
402
406
:validate_success_status ,
403
407
:validate_num_assertion ,
404
- :validate_no_duplicated_attributes ,
405
408
:validate_signed_elements ,
406
409
:validate_structure ,
410
+ :validate_no_duplicated_attributes ,
407
411
:validate_in_response_to ,
408
412
:validate_one_conditions ,
409
413
:validate_conditions ,
@@ -444,12 +448,14 @@ def validate_success_status
444
448
#
445
449
def validate_structure
446
450
structure_error_msg = "Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd"
447
- unless valid_saml? ( document , soft )
451
+
452
+ check_malformed_doc = check_malformed_doc_enabled?
453
+ unless valid_saml? ( document , soft , check_malformed_doc )
448
454
return append_error ( structure_error_msg )
449
455
end
450
456
451
457
unless decrypted_document . nil?
452
- unless valid_saml? ( decrypted_document , soft )
458
+ unless valid_saml? ( decrypted_document , soft , check_malformed_doc )
453
459
return append_error ( structure_error_msg )
454
460
end
455
461
end
@@ -841,31 +847,47 @@ def validate_name_id
841
847
true
842
848
end
843
849
850
+ def doc_to_validate
851
+ # If the response contains the signature, and the assertion was encrypted, validate the original SAML Response
852
+ # otherwise, review if the decrypted assertion contains a signature
853
+ sig_elements = REXML ::XPath . match (
854
+ document ,
855
+ "/p:Response[@ID=$id]/ds:Signature" ,
856
+ { "p" => PROTOCOL , "ds" => DSIG } ,
857
+ { 'id' => document . signed_element_id }
858
+ )
859
+
860
+ use_original = sig_elements . size == 1 || decrypted_document . nil?
861
+ doc = use_original ? document : decrypted_document
862
+ if !doc . processed
863
+ doc . cache_referenced_xml ( @soft , check_malformed_doc_enabled? )
864
+ end
865
+
866
+ return doc
867
+ end
868
+
844
869
# Validates the Signature
845
870
# @return [Boolean] True if not contains a Signature or if the Signature is valid, otherwise False if soft=True
846
871
# @raise [ValidationError] if soft == false and validation fails
847
872
#
848
873
def validate_signature
849
874
error_msg = "Invalid Signature on SAML Response"
850
875
851
- # If the response contains the signature, and the assertion was encrypted, validate the original SAML Response
852
- # otherwise, review if the decrypted assertion contains a signature
876
+ doc = doc_to_validate
877
+
853
878
sig_elements = REXML ::XPath . match (
854
879
document ,
855
880
"/p:Response[@ID=$id]/ds:Signature" ,
856
881
{ "p" => PROTOCOL , "ds" => DSIG } ,
857
882
{ 'id' => document . signed_element_id }
858
883
)
859
884
860
- use_original = sig_elements . size == 1 || decrypted_document . nil?
861
- doc = use_original ? document : decrypted_document
862
-
863
- # Check signature nodes
885
+ # Check signature node inside assertion
864
886
if sig_elements . nil? || sig_elements . size == 0
865
887
sig_elements = REXML ::XPath . match (
866
888
doc ,
867
889
"/p:Response/a:Assertion[@ID=$id]/ds:Signature" ,
868
- { "p" => PROTOCOL , "a" => ASSERTION , " ds"=> DSIG } ,
890
+ SAML_NAMESPACES . merge ( { " ds"=> DSIG } ) ,
869
891
{ 'id' => doc . signed_element_id }
870
892
)
871
893
end
@@ -943,24 +965,47 @@ def name_id_node
943
965
end
944
966
end
945
967
968
+ def get_cached_signed_assertion
969
+ xml = doc_to_validate . referenced_xml
970
+ empty_doc = REXML ::Document . new
971
+
972
+ return empty_doc if xml . nil? # when no signature/reference is found, return empty document
973
+
974
+ root = REXML ::Document . new ( xml ) . root
975
+
976
+ if root . attributes [ "ID" ] != doc_to_validate . signed_element_id
977
+ return empty_doc
978
+ end
979
+
980
+ assertion = empty_doc
981
+ if root . name == "Response"
982
+ if REXML ::XPath . first ( root , "a:Assertion" , { "a" => ASSERTION } )
983
+ assertion = REXML ::XPath . first ( root , "a:Assertion" , { "a" => ASSERTION } )
984
+ elsif REXML ::XPath . first ( root , "a:EncryptedAssertion" , { "a" => ASSERTION } )
985
+ assertion = decrypt_assertion ( REXML ::XPath . first ( root , "a:EncryptedAssertion" , { "a" => ASSERTION } ) )
986
+ end
987
+ elsif root . name == "Assertion"
988
+ assertion = root
989
+ end
990
+
991
+ assertion
992
+ end
993
+
994
+ def signed_assertion
995
+ @signed_assertion ||= get_cached_signed_assertion
996
+ end
997
+
946
998
# Extracts the first appearance that matchs the subelt (pattern)
947
999
# Search on any Assertion that is signed, or has a Response parent signed
948
1000
# @param subelt [String] The XPath pattern
949
1001
# @return [REXML::Element | nil] If any matches, return the Element
950
1002
#
951
1003
def xpath_first_from_signed_assertion ( subelt = nil )
952
- doc = decrypted_document . nil? ? document : decrypted_document
1004
+ doc = signed_assertion
953
1005
node = REXML ::XPath . first (
954
1006
doc ,
955
- "/p:Response/a:Assertion[@ID=$id]#{ subelt } " ,
956
- { "p" => PROTOCOL , "a" => ASSERTION } ,
957
- { 'id' => doc . signed_element_id }
958
- )
959
- node ||= REXML ::XPath . first (
960
- doc ,
961
- "/p:Response[@ID=$id]/a:Assertion#{ subelt } " ,
962
- { "p" => PROTOCOL , "a" => ASSERTION } ,
963
- { 'id' => doc . signed_element_id }
1007
+ "./#{ subelt } " ,
1008
+ SAML_NAMESPACES
964
1009
)
965
1010
node
966
1011
end
@@ -971,19 +1016,13 @@ def xpath_first_from_signed_assertion(subelt=nil)
971
1016
# @return [Array of REXML::Element] Return all matches
972
1017
#
973
1018
def xpath_from_signed_assertion ( subelt = nil )
974
- doc = decrypted_document . nil? ? document : decrypted_document
1019
+ doc = signed_assertion
975
1020
node = REXML ::XPath . match (
976
1021
doc ,
977
- "/p:Response/a:Assertion[@ID=$id]#{ subelt } " ,
978
- { "p" => PROTOCOL , "a" => ASSERTION } ,
979
- { 'id' => doc . signed_element_id }
1022
+ "./#{ subelt } " ,
1023
+ SAML_NAMESPACES
980
1024
)
981
- node . concat ( REXML ::XPath . match (
982
- doc ,
983
- "/p:Response[@ID=$id]/a:Assertion#{ subelt } " ,
984
- { "p" => PROTOCOL , "a" => ASSERTION } ,
985
- { 'id' => doc . signed_element_id }
986
- ) )
1025
+ node
987
1026
end
988
1027
989
1028
# Generates the decrypted_document
@@ -1017,7 +1056,7 @@ def decrypt_assertion_from_document(document_copy)
1017
1056
encrypted_assertion_node = REXML ::XPath . first (
1018
1057
document_copy ,
1019
1058
"(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)" ,
1020
- { "p" => PROTOCOL , "a" => ASSERTION }
1059
+ SAML_NAMESPACES
1021
1060
)
1022
1061
response_node . add ( decrypt_assertion ( encrypted_assertion_node ) )
1023
1062
encrypted_assertion_node . remove
@@ -1087,6 +1126,10 @@ def parse_time(node, attribute)
1087
1126
Time . parse ( node . attributes [ attribute ] )
1088
1127
end
1089
1128
end
1129
+
1130
+ def check_malformed_doc_enabled?
1131
+ check_malformed_doc? ( settings )
1132
+ end
1090
1133
end
1091
1134
end
1092
1135
end
0 commit comments