@@ -138,6 +138,140 @@ void authenticateWhenInvalidDestinationThenThrowAuthenticationException() {
138138 .satisfies (errorOf (Saml2ErrorCodes .INVALID_DESTINATION ));
139139 }
140140
141+ @ Test
142+ void authenticateWhenDestinationHasStandardHttpPortButLocationDoesNotThenSucceeds () {
143+ // Test scenario where destination includes explicit port 80 but location doesn't
144+ String destinationWithPort = "http://localhost:80/uaa/saml/SSO/alias/integration-saml-entity-id" ;
145+ String locationWithoutPort = "http://localhost/uaa/saml/SSO/alias/integration-saml-entity-id" ;
146+
147+ Response response = response (destinationWithPort , ASSERTING_PARTY_ENTITY_ID );
148+ Assertion assertion = assertion ();
149+ // Set the recipient in subject confirmation to match the location (without port)
150+ assertion .getSubject ().getSubjectConfirmations ().forEach (sc ->
151+ sc .getSubjectConfirmationData ().setRecipient (locationWithoutPort ));
152+ response .getAssertions ().add (assertion );
153+
154+ RelyingPartyRegistration .Builder registrationBuilder = verifying (registration ())
155+ .assertionConsumerServiceLocation (locationWithoutPort );
156+
157+ Saml2AuthenticationToken token = token (signed (response ), registrationBuilder );
158+
159+ // This should not throw an exception due to URL normalization
160+ assertThatNoException ().isThrownBy (() -> this .provider .authenticate (token ));
161+ }
162+
163+ @ Test
164+ void authenticateWhenDestinationHasStandardHttpsPortButLocationDoesNotThenSucceeds () {
165+ // Test scenario where destination includes explicit port 443 but location doesn't
166+ String destinationWithPort = "https://localhost:443/uaa/saml/SSO/alias/integration-saml-entity-id" ;
167+ String locationWithoutPort = "https://localhost/uaa/saml/SSO/alias/integration-saml-entity-id" ;
168+
169+ Response response = response (destinationWithPort , ASSERTING_PARTY_ENTITY_ID );
170+ Assertion assertion = assertion ();
171+ // Set the recipient in subject confirmation to match the location (without port)
172+ assertion .getSubject ().getSubjectConfirmations ().forEach (sc ->
173+ sc .getSubjectConfirmationData ().setRecipient (locationWithoutPort ));
174+ response .getAssertions ().add (assertion );
175+
176+ RelyingPartyRegistration .Builder registrationBuilder = verifying (registration ())
177+ .assertionConsumerServiceLocation (locationWithoutPort );
178+
179+ Saml2AuthenticationToken token = token (signed (response ), registrationBuilder );
180+
181+ // This should not throw an exception due to URL normalization
182+ assertThatNoException ().isThrownBy (() -> this .provider .authenticate (token ));
183+ }
184+
185+ @ Test
186+ void authenticateWhenLocationHasStandardPortButDestinationDoesNotThenSucceeds () {
187+ // Test reverse scenario where location includes explicit port but destination doesn't
188+ String destinationWithoutPort = "http://localhost/uaa/saml/SSO/alias/integration-saml-entity-id" ;
189+ String locationWithPort = "http://localhost:80/uaa/saml/SSO/alias/integration-saml-entity-id" ;
190+
191+ Response response = response (destinationWithoutPort , ASSERTING_PARTY_ENTITY_ID );
192+ Assertion assertion = assertion ();
193+ // Set the recipient in subject confirmation to match the location (with port)
194+ assertion .getSubject ().getSubjectConfirmations ().forEach (sc ->
195+ sc .getSubjectConfirmationData ().setRecipient (locationWithPort ));
196+ response .getAssertions ().add (assertion );
197+
198+ RelyingPartyRegistration .Builder registrationBuilder = verifying (registration ())
199+ .assertionConsumerServiceLocation (locationWithPort );
200+
201+ Saml2AuthenticationToken token = token (signed (response ), registrationBuilder );
202+
203+ // This should not throw an exception due to URL normalization
204+ assertThatNoException ().isThrownBy (() -> this .provider .authenticate (token ));
205+ }
206+
207+ @ Test
208+ void authenticateWhenNonStandardPortMismatchThenThrowsException () {
209+ // Test that non-standard ports still cause validation failure when they don't match
210+ String destinationWithPort8080 = "http://localhost:8080/uaa/saml/SSO/alias/integration-saml-entity-id" ;
211+ String locationWithPort8081 = "http://localhost:8081/uaa/saml/SSO/alias/integration-saml-entity-id" ;
212+
213+ Response response = response (destinationWithPort8080 , ASSERTING_PARTY_ENTITY_ID );
214+ Assertion assertion = assertion ();
215+ // Set the recipient in subject confirmation to match the location (port 8081)
216+ assertion .getSubject ().getSubjectConfirmations ().forEach (sc ->
217+ sc .getSubjectConfirmationData ().setRecipient (locationWithPort8081 ));
218+ response .getAssertions ().add (assertion );
219+
220+ RelyingPartyRegistration .Builder registrationBuilder = verifying (registration ())
221+ .assertionConsumerServiceLocation (locationWithPort8081 );
222+
223+ Saml2AuthenticationToken token = token (signed (response ), registrationBuilder );
224+
225+ // This should still throw an exception since ports don't match and aren't standard
226+ assertThatExceptionOfType (Saml2AuthenticationException .class )
227+ .isThrownBy (() -> this .provider .authenticate (token ))
228+ .satisfies (errorOf (Saml2ErrorCodes .INVALID_DESTINATION ));
229+ }
230+
231+ @ Test
232+ void authenticateWhenDestinationIsEmpty_thenSkipsValidationWithoutNPE () {
233+ // Test that when destination is empty/null, validation is skipped without throwing NPE
234+ String validLocation = DESTINATION ;
235+
236+ // Create response with null/empty destination
237+ Response response = response (null , ASSERTING_PARTY_ENTITY_ID );
238+ Assertion assertion = assertion ();
239+ assertion .getSubject ().getSubjectConfirmations ().forEach (sc ->
240+ sc .getSubjectConfirmationData ().setRecipient (validLocation ));
241+ response .getAssertions ().add (assertion );
242+
243+ RelyingPartyRegistration .Builder registrationBuilder = verifying (registration ())
244+ .assertionConsumerServiceLocation (validLocation );
245+
246+ Saml2AuthenticationToken token = token (signed (response ), registrationBuilder );
247+
248+ // This should not throw a NullPointerException when destination is null
249+ // The validation should be skipped since StringUtils.hasText(destination) returns false
250+ assertThatNoException ().isThrownBy (() -> this .provider .authenticate (token ));
251+ }
252+
253+ @ Test
254+ void authenticateWhenMalformedUrlsButIdentical_thenSucceeds () {
255+ // Test that malformed URLs that can't be normalized still work if they're identical
256+ // This tests the robustness of the comparison when normalization might fail
257+ String malformedUrl = "http://[malformed:url:with:brackets]/saml/SSO/alias/integration-saml-entity-id" ;
258+
259+ Response response = response (malformedUrl , ASSERTING_PARTY_ENTITY_ID );
260+ Assertion assertion = assertion ();
261+ assertion .getSubject ().getSubjectConfirmations ().forEach (sc ->
262+ sc .getSubjectConfirmationData ().setRecipient (malformedUrl ));
263+ response .getAssertions ().add (assertion );
264+
265+ RelyingPartyRegistration .Builder registrationBuilder = verifying (registration ())
266+ .assertionConsumerServiceLocation (malformedUrl );
267+
268+ Saml2AuthenticationToken token = token (signed (response ), registrationBuilder );
269+
270+ // Even with malformed URLs that normalization can't handle,
271+ // authentication should succeed if both URLs are identical
272+ assertThatNoException ().isThrownBy (() -> this .provider .authenticate (token ));
273+ }
274+
141275 @ Test
142276 void authenticateWhenNoAssertionsPresentThenThrowAuthenticationException () {
143277 Saml2AuthenticationToken token = token ();
0 commit comments