Prechádzať zdrojové kódy

DelegatingAuthenticationEntryPoint uses RequestMatcherEntry

Closes gh-17915
Rob Winch 2 týždňov pred
rodič
commit
9a3ae4b867

+ 9 - 10
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<H extends HttpSecurityBuilder<H>>
 
 	private AccessDeniedHandler accessDeniedHandler;
 
-	private LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> defaultEntryPointMappings = new LinkedHashMap<>();
+	private DelegatingAuthenticationEntryPoint.@Nullable Builder defaultEntryPoint;
 
 	private LinkedHashMap<RequestMatcher, AccessDeniedHandler> defaultDeniedHandlerMappings = new LinkedHashMap<>();
 
@@ -161,7 +163,10 @@ public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>>
 	 */
 	public ExceptionHandlingConfigurer<H> 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<H extends HttpSecurityBuilder<H>>
 	}
 
 	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();
 	}
 
 	/**

+ 6 - 6
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<B extends HttpSecurityBuilder<B>>
 	 */
 	public HttpBasicConfigurer() {
 		realmName(DEFAULT_REALM);
-		LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> 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
 	}
 
 	/**

+ 8 - 7
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<B extends HttpSecurityBuilder<B>>
 		RequestMatcher notXRequestedWith = new NegatedRequestMatcher(
 				new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest"));
 		RequestMatcher formLoginNotEnabled = getFormLoginNotEnabledRequestMatcher(http);
-		LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> 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) {

+ 8 - 6
config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java

@@ -339,13 +339,15 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
 				new OrRequestMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher);
 		RequestMatcher notXRequestedWith = new NegatedRequestMatcher(
 				new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest"));
-		LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> 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,

+ 126 - 4
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<RequestMatcher, AuthenticationEntryPoint> entryPoints;
+	private final List<RequestMatcherEntry<AuthenticationEntryPoint>> entryPoints;
 
 	@SuppressWarnings("NullAway.Init")
 	private AuthenticationEntryPoint defaultEntryPoint;
 
-	public DelegatingAuthenticationEntryPoint(LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> 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<AuthenticationEntryPoint>... 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<RequestMatcherEntry<AuthenticationEntryPoint>> 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<RequestMatcher, AuthenticationEntryPoint> 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<AuthenticationEntryPoint> 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<RequestMatcherEntry<AuthenticationEntryPoint>> entryPoints = new ArrayList<RequestMatcherEntry<AuthenticationEntryPoint>>();
+
+		/**
+		 * 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);
+		}
+
+	}
+
 }

+ 105 - 2
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<RequestMatcherEntry<AuthenticationEntryPoint>> 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<RequestMatcherEntry<AuthenticationEntryPoint>> entryPoints = List
+			.of(new RequestMatcherEntry<>(matcher, entryPoint));
+		assertThatIllegalArgumentException()
+			.isThrownBy(() -> new DelegatingAuthenticationEntryPoint(null, entryPoints));
+	}
+
+	@Test
+	public void constructorAepVargsWhenNullEntryPoints() {
+		RequestMatcherEntry<AuthenticationEntryPoint>[] entryPoints = null;
+		assertThatIllegalArgumentException().isThrownBy(
+				() -> new DelegatingAuthenticationEntryPoint(mock(AuthenticationEntryPoint.class), entryPoints));
+	}
+
+	@Test
+	public void constructorAepVargsWhenEmptyEntryPoints() {
+		RequestMatcherEntry<AuthenticationEntryPoint>[] 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<AuthenticationEntryPoint>[] 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<AuthenticationEntryPoint> 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<AuthenticationEntryPoint> firstEntry = new RequestMatcherEntry<>(firstRM, firstAEP);
+		AuthenticationEntryPoint secondAEP = mock(AuthenticationEntryPoint.class);
+		RequestMatcher secondRM = mock(RequestMatcher.class);
+		given(secondRM.matches(this.request)).willReturn(false);
+		RequestMatcherEntry<AuthenticationEntryPoint> 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<AuthenticationEntryPoint> firstEntry = new RequestMatcherEntry<>(firstRM, firstAEP);
+		AuthenticationEntryPoint secondAEP = mock(AuthenticationEntryPoint.class);
+		RequestMatcher secondRM = mock(RequestMatcher.class);
+		given(secondRM.matches(this.request)).willReturn(true);
+		RequestMatcherEntry<AuthenticationEntryPoint> 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);