Skip to content

Commit 4081893

Browse files
committed
Be able to invalidate a SAMLResponse if it contains InResponseTo value but no RequestId parameter provided at the is_valid method. See rejectUnsolicitedResponsesWithInResponseTo security parameter (By default deactivated)
1 parent 6b9faf5 commit 4081893

File tree

4 files changed

+71
-8
lines changed

4 files changed

+71
-8
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,11 @@ In addition to the required settings data (idp, sp), extra settings can be defin
414414
// Indicates a requirement for the AttributeStatement element
415415
"wantAttributeStatement": true,
416416

417+
// Rejects SAML responses with a InResponseTo attribute when request_id
418+
// not provided in the process_response method that later call the
419+
// response is_valid method with that parameter.
420+
"rejectUnsolicitedResponsesWithInResponseTo": false,
421+
417422
// Authentication context.
418423
// Set to false and no AuthContext will be sent in the AuthNRequest,
419424
// Set true or don't present this parameter and you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'

src/onelogin/saml2/response.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -133,14 +133,19 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
133133
security = self.__settings.get_security_data()
134134
current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data)
135135

136-
# Check if the InResponseTo of the Response matchs the ID of the AuthNRequest (requestId) if provided
137136
in_response_to = self.document.get('InResponseTo', None)
138-
if in_response_to is not None and request_id is not None:
139-
if in_response_to != request_id:
140-
raise OneLogin_Saml2_ValidationError(
141-
'The InResponseTo of the Response: %s, does not match the ID of the AuthNRequest sent by the SP: %s' % (in_response_to, request_id),
142-
OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO
143-
)
137+
if request_id is None and in_response_to is not None and security.get('rejectUnsolicitedResponsesWithInResponseTo', False):
138+
raise OneLogin_Saml2_ValidationError(
139+
'The Response has an InResponseTo attribute: %s while no InResponseTo was expected' % in_response_to,
140+
OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO
141+
)
142+
143+
# Check if the InResponseTo of the Response matchs the ID of the AuthNRequest (requestId) if provided
144+
if request_id is not None and in_response_to != request_id:
145+
raise OneLogin_Saml2_ValidationError(
146+
'The InResponseTo of the Response: %s, does not match the ID of the AuthNRequest sent by the SP: %s' % (in_response_to, request_id),
147+
OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO
148+
)
144149

145150
if not self.encrypted and security.get('wantAssertionsEncrypted', False):
146151
raise OneLogin_Saml2_ValidationError(
@@ -244,7 +249,9 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
244249
continue
245250
else:
246251
irt = sc_data.get('InResponseTo', None)
247-
if in_response_to and irt and irt != in_response_to:
252+
if (in_response_to is None and irt is not None and
253+
security.get('rejectUnsolicitedResponsesWithInResponseTo', False)) or \
254+
in_response_to and irt and irt != in_response_to:
248255
continue
249256
recipient = sc_data.get('Recipient', None)
250257
if recipient and current_url not in recipient:

src/onelogin/saml2/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,9 @@ def __add_default_values(self):
280280
# NameID element expected
281281
self.__security.setdefault('wantNameId', True)
282282

283+
# SAML responses with a InResponseTo attribute not rejected when requestId not passed
284+
self.__security.setdefault('rejectUnsolicitedResponsesWithInResponseTo', False)
285+
283286
# Encrypt expected
284287
self.__security.setdefault('wantAssertionsEncrypted', False)
285288
self.__security.setdefault('wantNameIdEncrypted', False)

tests/src/OneLogin/saml2_tests/response_test.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,6 +1173,54 @@ def testIsInValidRequestId(self):
11731173
response.is_valid(request_data, valid_request_id)
11741174
self.assertEqual('No Signature found. SAML Response rejected', response.get_error())
11751175

1176+
def testRejectUnsolicitedResponsesWithInResponseTo(self):
1177+
settings_info = self.loadSettingsJSON()
1178+
settings_info['strict'] = True
1179+
settings_info['security']['rejectUnsolicitedResponsesWithInResponseTo'] = False
1180+
settings = OneLogin_Saml2_Settings(settings_info)
1181+
request_data = {
1182+
'http_host': 'stuff.com',
1183+
'script_name': 'endpoints/endpoints/acs.php'
1184+
}
1185+
1186+
xml = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64'))
1187+
response = OneLogin_Saml2_Response(settings, xml)
1188+
response.is_valid(request_data)
1189+
self.assertEqual('No Signature found. SAML Response rejected', response.get_error())
1190+
1191+
settings_info['security']['rejectUnsolicitedResponsesWithInResponseTo'] = True
1192+
settings = OneLogin_Saml2_Settings(settings_info)
1193+
response = OneLogin_Saml2_Response(settings, xml)
1194+
response.is_valid(request_data)
1195+
self.assertEqual('The Response has an InResponseTo attribute: _57bcbf70-7b1f-012e-c821-782bcb13bb38 while no InResponseTo was expected', response.get_error())
1196+
1197+
settings_info['idp']['entityId'] = 'https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php'
1198+
settings_info['sp']['entityId'] = 'https://pitbulk.no-ip.org/newonelogin/demo1/metadata.php'
1199+
request_data = {
1200+
'https': 'on',
1201+
'http_host': 'pitbulk.no-ip.org',
1202+
'script_name': 'newonelogin/demo1/index.php?acs'
1203+
}
1204+
not_on_or_after = datetime.strptime('2014-02-19T09:37:01Z', '%Y-%m-%dT%H:%M:%SZ')
1205+
not_on_or_after -= timedelta(seconds=150)
1206+
1207+
# InResponseTo on the SubjectConfirmation only
1208+
xml = self.file_contents(join(self.data_path, 'responses', 'valid_response_without_inresponseto.xml.base64'))
1209+
settings_info['security']['rejectUnsolicitedResponsesWithInResponseTo'] = False
1210+
settings = OneLogin_Saml2_Settings(settings_info)
1211+
response = OneLogin_Saml2_Response(settings, xml)
1212+
1213+
with freeze_time(not_on_or_after):
1214+
self.assertTrue(response.is_valid(request_data))
1215+
1216+
settings_info['security']['rejectUnsolicitedResponsesWithInResponseTo'] = True
1217+
settings = OneLogin_Saml2_Settings(settings_info)
1218+
response = OneLogin_Saml2_Response(settings, xml)
1219+
1220+
with freeze_time(not_on_or_after):
1221+
self.assertFalse(response.is_valid(request_data))
1222+
self.assertEquals("A valid SubjectConfirmation was not found on this Response", response.get_error())
1223+
11761224
def testIsInValidSignIssues(self):
11771225
"""
11781226
Tests the is_valid method of the OneLogin_Saml2_Response class

0 commit comments

Comments
 (0)