@@ -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