From 9a3ae4b8670481a96ce4f524e28d9b36c2026fa2 Mon Sep 17 00:00:00 2001 From: Rob Winch <362503+rwinch@users.noreply.github.com> Date: Mon, 15 Sep 2025 19:23:48 -0500 Subject: [PATCH] DelegatingAuthenticationEntryPoint uses RequestMatcherEntry Closes gh-17915 --- .../ExceptionHandlingConfigurer.java | 19 ++- .../web/configurers/HttpBasicConfigurer.java | 12 +- .../oauth2/client/OAuth2LoginConfigurer.java | 15 +- .../saml2/Saml2LoginConfigurer.java | 14 +- .../DelegatingAuthenticationEntryPoint.java | 130 +++++++++++++++++- ...legatingAuthenticationEntryPointTests.java | 107 +++++++++++++- 6 files changed, 262 insertions(+), 35 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java index 5dc63fecdf..6b6a2f1f7c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java @@ -18,6 +18,8 @@ package org.springframework.security.config.annotation.web.configurers; import java.util.LinkedHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -71,7 +73,7 @@ public final class ExceptionHandlingConfigurer> private AccessDeniedHandler accessDeniedHandler; - private LinkedHashMap defaultEntryPointMappings = new LinkedHashMap<>(); + private DelegatingAuthenticationEntryPoint.@Nullable Builder defaultEntryPoint; private LinkedHashMap defaultDeniedHandlerMappings = new LinkedHashMap<>(); @@ -161,7 +163,10 @@ public final class ExceptionHandlingConfigurer> */ public ExceptionHandlingConfigurer defaultAuthenticationEntryPointFor(AuthenticationEntryPoint entryPoint, RequestMatcher preferredMatcher) { - this.defaultEntryPointMappings.put(preferredMatcher, entryPoint); + if (this.defaultEntryPoint == null) { + this.defaultEntryPoint = DelegatingAuthenticationEntryPoint.builder(); + } + this.defaultEntryPoint.addEntryPointFor(entryPoint, preferredMatcher); return this; } @@ -235,16 +240,10 @@ public final class ExceptionHandlingConfigurer> } private AuthenticationEntryPoint createDefaultEntryPoint(H http) { - if (this.defaultEntryPointMappings.isEmpty()) { + if (this.defaultEntryPoint == null) { return new Http403ForbiddenEntryPoint(); } - if (this.defaultEntryPointMappings.size() == 1) { - return this.defaultEntryPointMappings.values().iterator().next(); - } - DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint( - this.defaultEntryPointMappings); - entryPoint.setDefaultEntryPoint(this.defaultEntryPointMappings.values().iterator().next()); - return entryPoint; + return this.defaultEntryPoint.build(); } /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurer.java index ad03ae6052..e2f2a8ec59 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurer.java @@ -18,7 +18,6 @@ package org.springframework.security.config.annotation.web.configurers; import java.util.Arrays; import java.util.Collections; -import java.util.LinkedHashMap; import jakarta.servlet.http.HttpServletRequest; @@ -103,11 +102,12 @@ public final class HttpBasicConfigurer> */ public HttpBasicConfigurer() { realmName(DEFAULT_REALM); - LinkedHashMap entryPoints = new LinkedHashMap<>(); - entryPoints.put(X_REQUESTED_WITH, new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)); - DelegatingAuthenticationEntryPoint defaultEntryPoint = new DelegatingAuthenticationEntryPoint(entryPoints); - defaultEntryPoint.setDefaultEntryPoint(this.basicAuthEntryPoint); - this.authenticationEntryPoint = defaultEntryPoint; + // @formatter:off + this.authenticationEntryPoint = DelegatingAuthenticationEntryPoint.builder() + .addEntryPointFor(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), X_REQUESTED_WITH) + .defaultEntryPoint(this.basicAuthEntryPoint) + .build(); + // @formatter:on } /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java index 299dfd0307..494f75109a 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java @@ -19,7 +19,6 @@ package org.springframework.security.config.annotation.web.configurers.oauth2.cl import java.lang.reflect.Field; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.Map; import jakarta.servlet.http.HttpServletRequest; @@ -553,13 +552,15 @@ public final class OAuth2LoginConfigurer> RequestMatcher notXRequestedWith = new NegatedRequestMatcher( new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest")); RequestMatcher formLoginNotEnabled = getFormLoginNotEnabledRequestMatcher(http); - LinkedHashMap entryPoints = new LinkedHashMap<>(); LoginUrlAuthenticationEntryPoint loginUrlEntryPoint = new LoginUrlAuthenticationEntryPoint(providerLoginPage); - entryPoints.put(new AndRequestMatcher(notXRequestedWith, new NegatedRequestMatcher(defaultLoginPageMatcher), - formLoginNotEnabled), loginUrlEntryPoint); - DelegatingAuthenticationEntryPoint loginEntryPoint = new DelegatingAuthenticationEntryPoint(entryPoints); - loginEntryPoint.setDefaultEntryPoint(this.getAuthenticationEntryPoint()); - return loginEntryPoint; + RequestMatcher loginUrlMatcher = new AndRequestMatcher(notXRequestedWith, + new NegatedRequestMatcher(defaultLoginPageMatcher), formLoginNotEnabled); + // @formatter:off + return DelegatingAuthenticationEntryPoint.builder() + .addEntryPointFor(loginUrlEntryPoint, loginUrlMatcher) + .defaultEntryPoint(getAuthenticationEntryPoint()) + .build(); + // @formatter:on } private RequestMatcher getFormLoginNotEnabledRequestMatcher(B http) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java index 8f4f4c1ad2..fbbb3f73a7 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java @@ -339,13 +339,15 @@ public final class Saml2LoginConfigurer> new OrRequestMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher); RequestMatcher notXRequestedWith = new NegatedRequestMatcher( new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest")); - LinkedHashMap entryPoints = new LinkedHashMap<>(); LoginUrlAuthenticationEntryPoint loginUrlEntryPoint = new LoginUrlAuthenticationEntryPoint(providerLoginPage); - entryPoints.put(new AndRequestMatcher(notXRequestedWith, new NegatedRequestMatcher(defaultLoginPageMatcher)), - loginUrlEntryPoint); - DelegatingAuthenticationEntryPoint loginEntryPoint = new DelegatingAuthenticationEntryPoint(entryPoints); - loginEntryPoint.setDefaultEntryPoint(this.getAuthenticationEntryPoint()); - return loginEntryPoint; + RequestMatcher loginUrlMatcher = new AndRequestMatcher(notXRequestedWith, + new NegatedRequestMatcher(defaultLoginPageMatcher)); + // @formatter:off + return DelegatingAuthenticationEntryPoint.builder() + .addEntryPointFor(loginUrlEntryPoint, loginUrlMatcher) + .defaultEntryPoint(getAuthenticationEntryPoint()) + .build(); + // @formatter:on } private void setAuthenticationRequestRepository(B http, diff --git a/web/src/main/java/org/springframework/security/web/authentication/DelegatingAuthenticationEntryPoint.java b/web/src/main/java/org/springframework/security/web/authentication/DelegatingAuthenticationEntryPoint.java index bb8d845475..da1ef7ccf2 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/DelegatingAuthenticationEntryPoint.java +++ b/web/src/main/java/org/springframework/security/web/authentication/DelegatingAuthenticationEntryPoint.java @@ -17,13 +17,18 @@ package org.springframework.security.web.authentication; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedHashMap; +import java.util.List; +import java.util.stream.Collectors; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.log.LogMessage; @@ -32,6 +37,7 @@ import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.util.matcher.ELRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcherEditor; +import org.springframework.security.web.util.matcher.RequestMatcherEntry; import org.springframework.util.Assert; /** @@ -64,22 +70,63 @@ public class DelegatingAuthenticationEntryPoint implements AuthenticationEntryPo private static final Log logger = LogFactory.getLog(DelegatingAuthenticationEntryPoint.class); - private final LinkedHashMap entryPoints; + private final List> entryPoints; @SuppressWarnings("NullAway.Init") private AuthenticationEntryPoint defaultEntryPoint; - public DelegatingAuthenticationEntryPoint(LinkedHashMap entryPoints) { + /** + * Creates a new instance with the provided mappings. + * @param entryPoints the mapping of {@link RequestMatcher} to + * {@link AuthenticationEntryPoint}. Cannot be null or empty. + * @param defaultEntryPoint the default {@link AuthenticationEntryPoint}. Cannot be + * null. + */ + public DelegatingAuthenticationEntryPoint(AuthenticationEntryPoint defaultEntryPoint, + RequestMatcherEntry... entryPoints) { + Assert.notEmpty(entryPoints, "entryPoints cannot be empty"); + Assert.notNull(defaultEntryPoint, "defaultEntryPoint cannot be null"); + this.entryPoints = Arrays.asList(entryPoints); + this.defaultEntryPoint = defaultEntryPoint; + } + + /** + * Creates a new instance with the provided mappings. + * @param defaultEntryPoint the default {@link AuthenticationEntryPoint}. Cannot be + * null. + * @param entryPoints the mapping of {@link RequestMatcher} to + * {@link AuthenticationEntryPoint}. Cannot be null or empty. + */ + public DelegatingAuthenticationEntryPoint(AuthenticationEntryPoint defaultEntryPoint, + List> entryPoints) { + Assert.notEmpty(entryPoints, "entryPoints cannot be empty"); + Assert.notNull(defaultEntryPoint, "defaultEntryPoint cannot be null"); this.entryPoints = entryPoints; + this.defaultEntryPoint = defaultEntryPoint; + } + + /** + * Creates a new instance. + * @param entryPoints + * @deprecated Use + * {@link #DelegatingAuthenticationEntryPoint(AuthenticationEntryPoint, List)} + */ + @Deprecated(forRemoval = true) + public DelegatingAuthenticationEntryPoint(LinkedHashMap entryPoints) { + this.entryPoints = entryPoints.entrySet() + .stream() + .map((e) -> new RequestMatcherEntry<>(e.getKey(), e.getValue())) + .collect(Collectors.toList()); } @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { - for (RequestMatcher requestMatcher : this.entryPoints.keySet()) { + for (RequestMatcherEntry entry : this.entryPoints) { + RequestMatcher requestMatcher = entry.getRequestMatcher(); logger.debug(LogMessage.format("Trying to match using %s", requestMatcher)); if (requestMatcher.matches(request)) { - AuthenticationEntryPoint entryPoint = this.entryPoints.get(requestMatcher); + AuthenticationEntryPoint entryPoint = entry.getEntry(); logger.debug(LogMessage.format("Match found! Executing %s", entryPoint)); entryPoint.commence(request, response, authException); return; @@ -92,7 +139,10 @@ public class DelegatingAuthenticationEntryPoint implements AuthenticationEntryPo /** * EntryPoint which is used when no RequestMatcher returned true + * @deprecated Use + * {@link #DelegatingAuthenticationEntryPoint(AuthenticationEntryPoint, List)} */ + @Deprecated(forRemoval = true) public void setDefaultEntryPoint(AuthenticationEntryPoint defaultEntryPoint) { this.defaultEntryPoint = defaultEntryPoint; } @@ -103,4 +153,76 @@ public class DelegatingAuthenticationEntryPoint implements AuthenticationEntryPo Assert.notNull(this.defaultEntryPoint, "defaultEntryPoint must be specified"); } + /** + * Creates a new {@link Builder} + * @return the new {@link Builder} + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Used to build a new instance of {@link DelegatingAuthenticationEntryPoint}. + * + * @author Rob Winch + * @since 7.0 + */ + public static class Builder { + + private @Nullable AuthenticationEntryPoint defaultEntryPoint; + + private List> entryPoints = new ArrayList>(); + + /** + * Set the default {@link AuthenticationEntryPoint} if none match. The default is + * to use the first {@link AuthenticationEntryPoint} added in + * {@link #addEntryPointFor(AuthenticationEntryPoint, RequestMatcher)}. + * @param defaultEntryPoint the default {@link AuthenticationEntryPoint} to use. + * @return the {@link Builder} for further customization. + */ + public Builder defaultEntryPoint(@Nullable AuthenticationEntryPoint defaultEntryPoint) { + this.defaultEntryPoint = defaultEntryPoint; + return this; + } + + /** + * Adds an {@link AuthenticationEntryPoint} for the provided + * {@link RequestMatcher}. + * @param entryPoint the {@link AuthenticationEntryPoint} to use. Cannot be null. + * @param requestMatcher the {@link RequestMatcher} to use. Cannot be null. + * @return the {@link Builder} for further customization. + */ + public Builder addEntryPointFor(AuthenticationEntryPoint entryPoint, RequestMatcher requestMatcher) { + Assert.notNull(entryPoint, "entryPoint cannot be null"); + Assert.notNull(requestMatcher, "requestMatcher cannot be null"); + this.entryPoints.add(new RequestMatcherEntry<>(requestMatcher, entryPoint)); + return this; + } + + /** + * Builds the {@link AuthenticationEntryPoint}. If the + * {@link #defaultEntryPoint(AuthenticationEntryPoint)} is not set, then the first + * {@link #addEntryPointFor(AuthenticationEntryPoint, RequestMatcher)} is used as + * the default. If the {@link #defaultEntryPoint(AuthenticationEntryPoint)} is not + * set and there is only a single + * {@link #addEntryPointFor(AuthenticationEntryPoint, RequestMatcher)}, then the + * {@link AuthenticationEntryPoint} is returned rather than wrapping it in + * {@link DelegatingAuthenticationEntryPoint}. + * @return the {@link AuthenticationEntryPoint} to use. + */ + public AuthenticationEntryPoint build() { + Assert.notEmpty(this.entryPoints, "entryPoints cannot be empty"); + AuthenticationEntryPoint defaultEntryPoint = this.defaultEntryPoint; + if (defaultEntryPoint == null) { + AuthenticationEntryPoint firstAuthenticationEntryPoint = this.entryPoints.get(0).getEntry(); + if (this.entryPoints.size() == 1) { + return firstAuthenticationEntryPoint; + } + defaultEntryPoint = firstAuthenticationEntryPoint; + } + return new DelegatingAuthenticationEntryPoint(defaultEntryPoint, this.entryPoints); + } + + } + } diff --git a/web/src/test/java/org/springframework/security/web/authentication/DelegatingAuthenticationEntryPointTests.java b/web/src/test/java/org/springframework/security/web/authentication/DelegatingAuthenticationEntryPointTests.java index ce35c38f9b..963eba746f 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/DelegatingAuthenticationEntryPointTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/DelegatingAuthenticationEntryPointTests.java @@ -16,7 +16,9 @@ package org.springframework.security.web.authentication; +import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.BeforeEach; @@ -25,7 +27,9 @@ import org.junit.jupiter.api.Test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcherEntry; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -52,8 +56,6 @@ public class DelegatingAuthenticationEntryPointTests { public void before() { this.defaultEntryPoint = mock(AuthenticationEntryPoint.class); this.entryPoints = new LinkedHashMap<>(); - this.daep = new DelegatingAuthenticationEntryPoint(this.entryPoints); - this.daep.setDefaultEntryPoint(this.defaultEntryPoint); } @Test @@ -62,6 +64,8 @@ public class DelegatingAuthenticationEntryPointTests { RequestMatcher firstRM = mock(RequestMatcher.class); given(firstRM.matches(this.request)).willReturn(false); this.entryPoints.put(firstRM, firstAEP); + this.daep = new DelegatingAuthenticationEntryPoint(this.entryPoints); + this.daep.setDefaultEntryPoint(this.defaultEntryPoint); this.daep.commence(this.request, null, null); verify(this.defaultEntryPoint).commence(this.request, null, null); verify(firstAEP, never()).commence(this.request, null, null); @@ -76,6 +80,8 @@ public class DelegatingAuthenticationEntryPointTests { given(firstRM.matches(this.request)).willReturn(true); this.entryPoints.put(firstRM, firstAEP); this.entryPoints.put(secondRM, secondAEP); + this.daep = new DelegatingAuthenticationEntryPoint(this.entryPoints); + this.daep.setDefaultEntryPoint(this.defaultEntryPoint); this.daep.commence(this.request, null, null); verify(firstAEP).commence(this.request, null, null); verify(secondAEP, never()).commence(this.request, null, null); @@ -93,6 +99,103 @@ public class DelegatingAuthenticationEntryPointTests { given(secondRM.matches(this.request)).willReturn(true); this.entryPoints.put(firstRM, firstAEP); this.entryPoints.put(secondRM, secondAEP); + this.daep = new DelegatingAuthenticationEntryPoint(this.entryPoints); + this.daep.setDefaultEntryPoint(this.defaultEntryPoint); + this.daep.commence(this.request, null, null); + verify(secondAEP).commence(this.request, null, null); + verify(firstAEP, never()).commence(this.request, null, null); + verify(this.defaultEntryPoint, never()).commence(this.request, null, null); + } + + @Test + public void constructorAepListWhenNullEntryPoints() { + List> entryPoints = null; + assertThatIllegalArgumentException().isThrownBy( + () -> new DelegatingAuthenticationEntryPoint(mock(AuthenticationEntryPoint.class), entryPoints)); + } + + @Test + public void constructorAepListWhenEmptyEntryPoints() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new DelegatingAuthenticationEntryPoint(mock(AuthenticationEntryPoint.class), + Collections.emptyList())); + } + + @Test + public void constructorAepListWhenNullDefaultEntryPoint() { + AuthenticationEntryPoint entryPoint = mock(AuthenticationEntryPoint.class); + RequestMatcher matcher = mock(RequestMatcher.class); + List> entryPoints = List + .of(new RequestMatcherEntry<>(matcher, entryPoint)); + assertThatIllegalArgumentException() + .isThrownBy(() -> new DelegatingAuthenticationEntryPoint(null, entryPoints)); + } + + @Test + public void constructorAepVargsWhenNullEntryPoints() { + RequestMatcherEntry[] entryPoints = null; + assertThatIllegalArgumentException().isThrownBy( + () -> new DelegatingAuthenticationEntryPoint(mock(AuthenticationEntryPoint.class), entryPoints)); + } + + @Test + public void constructorAepVargsWhenEmptyEntryPoints() { + RequestMatcherEntry[] entryPoints = new RequestMatcherEntry[0]; + assertThatIllegalArgumentException().isThrownBy( + () -> new DelegatingAuthenticationEntryPoint(mock(AuthenticationEntryPoint.class), entryPoints)); + } + + @Test + public void constructorAepVargsWhenNullDefaultEntryPoint() { + AuthenticationEntryPoint entryPoint = mock(AuthenticationEntryPoint.class); + RequestMatcher matcher = mock(RequestMatcher.class); + RequestMatcherEntry[] entryPoints = new RequestMatcherEntry[] { + new RequestMatcherEntry<>(matcher, entryPoint) }; + assertThatIllegalArgumentException() + .isThrownBy(() -> new DelegatingAuthenticationEntryPoint(null, entryPoints)); + } + + @Test + public void commenceWhenNoMatchThenDefaultEntryPoint() throws Exception { + AuthenticationEntryPoint firstAEP = mock(AuthenticationEntryPoint.class); + RequestMatcher firstRM = mock(RequestMatcher.class); + given(firstRM.matches(this.request)).willReturn(false); + RequestMatcherEntry entry = new RequestMatcherEntry<>(firstRM, firstAEP); + this.daep = new DelegatingAuthenticationEntryPoint(this.defaultEntryPoint, entry); + this.daep.commence(this.request, null, null); + verify(this.defaultEntryPoint).commence(this.request, null, null); + verify(firstAEP, never()).commence(this.request, null, null); + } + + @Test + public void commenceWhenMatchFirstEntryPointThenOthersNotInvoked() throws Exception { + AuthenticationEntryPoint firstAEP = mock(AuthenticationEntryPoint.class); + RequestMatcher firstRM = mock(RequestMatcher.class); + given(firstRM.matches(this.request)).willReturn(true); + RequestMatcherEntry firstEntry = new RequestMatcherEntry<>(firstRM, firstAEP); + AuthenticationEntryPoint secondAEP = mock(AuthenticationEntryPoint.class); + RequestMatcher secondRM = mock(RequestMatcher.class); + given(secondRM.matches(this.request)).willReturn(false); + RequestMatcherEntry secondEntry = new RequestMatcherEntry<>(firstRM, firstAEP); + this.daep = new DelegatingAuthenticationEntryPoint(this.defaultEntryPoint, firstEntry, secondEntry); + this.daep.commence(this.request, null, null); + verify(firstAEP).commence(this.request, null, null); + verify(secondAEP, never()).commence(this.request, null, null); + verify(this.defaultEntryPoint, never()).commence(this.request, null, null); + verify(secondRM, never()).matches(this.request); + } + + @Test + public void commenceWhenSecondMatchesThenDefaultNotInvoked() throws Exception { + AuthenticationEntryPoint firstAEP = mock(AuthenticationEntryPoint.class); + RequestMatcher firstRM = mock(RequestMatcher.class); + given(firstRM.matches(this.request)).willReturn(false); + RequestMatcherEntry firstEntry = new RequestMatcherEntry<>(firstRM, firstAEP); + AuthenticationEntryPoint secondAEP = mock(AuthenticationEntryPoint.class); + RequestMatcher secondRM = mock(RequestMatcher.class); + given(secondRM.matches(this.request)).willReturn(true); + RequestMatcherEntry secondEntry = new RequestMatcherEntry<>(secondRM, secondAEP); + this.daep = new DelegatingAuthenticationEntryPoint(this.defaultEntryPoint, firstEntry, secondEntry); this.daep.commence(this.request, null, null); verify(secondAEP).commence(this.request, null, null); verify(firstAEP, never()).commence(this.request, null, null);