Skip to content

Commit 9ec0c9e

Browse files
committed
Validate InResponseTo and require SubjectConfirmationData in SAML assertion subject confirmations
1 parent 54f63a1 commit 9ec0c9e

2 files changed

Lines changed: 168 additions & 0 deletions

File tree

src/Saml/SamlUtils.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,30 @@ public function validateAssertionConditions(
193193
);
194194
}
195195

196+
foreach ($response->getAllAssertions() as $assertion) {
197+
$subject = $assertion->getSubject();
198+
199+
if ($subject === null) {
200+
continue;
201+
}
202+
203+
foreach ($subject->getAllSubjectConfirmations() as $subject_confirmation) {
204+
$confirmation_data = $subject_confirmation->getSubjectConfirmationData();
205+
206+
if ($confirmation_data === null) {
207+
throw new InvalidSamlResponseException(
208+
'SubjectConfirmation is missing SubjectConfirmationData.',
209+
);
210+
}
211+
212+
if ($confirmation_data->getInResponseTo() !== $in_response_to) {
213+
throw new InvalidSamlResponseException(
214+
'SubjectConfirmationData InResponseTo is missing or does not match the response.',
215+
);
216+
}
217+
}
218+
}
219+
196220
$now = time();
197221
$skew = 60;
198222

test/src/Saml/InResponseToValidationTest.php

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,150 @@ public function testReplayIsRejected(): void
146146
);
147147
}
148148

149+
public function testSubjectConfirmationDataInResponseToPassesValidation(): void
150+
{
151+
$message_id = '123abc';
152+
$authn_request_id = '_f6d3b434-629a-4c91-998a-7889e496359b';
153+
154+
$response_html = $this->generateResponse('owner@company.com', $message_id, $authn_request_id);
155+
156+
if (preg_match('/name="SAMLResponse" value="([^"]+)"/', $response_html, $matches)) {
157+
$saml_response_b64 = $matches[1];
158+
} else {
159+
$this->fail('Could not extract SAMLResponse from HTML');
160+
}
161+
162+
$this->store->save($authn_request_id, 3600);
163+
164+
$payload = ['SAMLResponse' => $saml_response_b64];
165+
$parsed_response = $this->saml_utils->parseSamlResponse(
166+
$payload,
167+
$this->idp_certificate,
168+
$this->expected_destination,
169+
$this->expected_audience,
170+
$this->store
171+
);
172+
173+
$this->assertSame($authn_request_id, $parsed_response->getInResponseTo());
174+
}
175+
176+
public function testMissingSubjectConfirmationDataInResponseToIsRejected(): void
177+
{
178+
$message_id = '123abc';
179+
$authn_request_id = '_f6d3b434-629a-4c91-998a-7889e496359b';
180+
181+
$response_html = $this->generateResponse('owner@company.com', $message_id, $authn_request_id);
182+
183+
if (preg_match('/name="SAMLResponse" value="([^"]+)"/', $response_html, $matches)) {
184+
$saml_response_b64 = $matches[1];
185+
} else {
186+
$this->fail('Could not extract SAMLResponse from HTML');
187+
}
188+
189+
$xml = base64_decode($saml_response_b64);
190+
$xml = preg_replace(
191+
'/(<[a-zA-Z:]*SubjectConfirmationData)\s+InResponseTo="[^"]*"/',
192+
'$1',
193+
$xml,
194+
);
195+
196+
$deserialization_context = new DeserializationContext();
197+
$deserialization_context->getDocument()->loadXML($xml);
198+
199+
$saml_response = new Response();
200+
$saml_response->deserialize($deserialization_context->getDocument()->firstChild, $deserialization_context);
201+
202+
$this->store->save($authn_request_id, 3600);
203+
204+
$this->expectException(InvalidSamlResponseException::class);
205+
$this->expectExceptionMessage('SubjectConfirmationData InResponseTo is missing or does not match the response.');
206+
207+
$this->saml_utils->validateAssertionConditions(
208+
$saml_response,
209+
$this->expected_destination,
210+
$this->expected_audience,
211+
$this->store,
212+
);
213+
}
214+
215+
public function testMismatchedSubjectConfirmationDataInResponseToIsRejected(): void
216+
{
217+
$message_id = '123abc';
218+
$authn_request_id = '_f6d3b434-629a-4c91-998a-7889e496359b';
219+
220+
$response_html = $this->generateResponse('owner@company.com', $message_id, $authn_request_id);
221+
222+
if (preg_match('/name="SAMLResponse" value="([^"]+)"/', $response_html, $matches)) {
223+
$saml_response_b64 = $matches[1];
224+
} else {
225+
$this->fail('Could not extract SAMLResponse from HTML');
226+
}
227+
228+
$xml = base64_decode($saml_response_b64);
229+
$xml = preg_replace(
230+
'/(<[a-zA-Z:]*SubjectConfirmationData[^>]*)\bInResponseTo="[^"]*"/',
231+
'$1InResponseTo="_tampered_id"',
232+
$xml,
233+
);
234+
235+
$deserialization_context = new DeserializationContext();
236+
$deserialization_context->getDocument()->loadXML($xml);
237+
238+
$saml_response = new Response();
239+
$saml_response->deserialize($deserialization_context->getDocument()->firstChild, $deserialization_context);
240+
241+
$this->store->save($authn_request_id, 3600);
242+
243+
$this->expectException(InvalidSamlResponseException::class);
244+
$this->expectExceptionMessage('SubjectConfirmationData InResponseTo is missing or does not match the response.');
245+
246+
$this->saml_utils->validateAssertionConditions(
247+
$saml_response,
248+
$this->expected_destination,
249+
$this->expected_audience,
250+
$this->store,
251+
);
252+
}
253+
254+
public function testMissingSubjectConfirmationDataElementIsRejected(): void
255+
{
256+
$message_id = '123abc';
257+
$authn_request_id = '_f6d3b434-629a-4c91-998a-7889e496359b';
258+
259+
$response_html = $this->generateResponse('owner@company.com', $message_id, $authn_request_id);
260+
261+
if (preg_match('/name="SAMLResponse" value="([^"]+)"/', $response_html, $matches)) {
262+
$saml_response_b64 = $matches[1];
263+
} else {
264+
$this->fail('Could not extract SAMLResponse from HTML');
265+
}
266+
267+
$xml = base64_decode($saml_response_b64);
268+
$xml = preg_replace(
269+
'/<[a-zA-Z:]*SubjectConfirmationData[^>]*\/>/',
270+
'',
271+
$xml,
272+
);
273+
274+
$deserialization_context = new DeserializationContext();
275+
$deserialization_context->getDocument()->loadXML($xml);
276+
277+
$saml_response = new Response();
278+
$saml_response->deserialize($deserialization_context->getDocument()->firstChild, $deserialization_context);
279+
280+
$this->store->save($authn_request_id, 3600);
281+
282+
$this->expectException(InvalidSamlResponseException::class);
283+
$this->expectExceptionMessage('SubjectConfirmation is missing SubjectConfirmationData.');
284+
285+
$this->saml_utils->validateAssertionConditions(
286+
$saml_response,
287+
$this->expected_destination,
288+
$this->expected_audience,
289+
$this->store,
290+
);
291+
}
292+
149293
public function testMissingInResponseToIsRejectedWhenStoreIsProvided(): void
150294
{
151295
$message_id = '123abc';

0 commit comments

Comments
 (0)