Skip to content

Commit 9a3ae4b

Browse files
committed
DelegatingAuthenticationEntryPoint uses RequestMatcherEntry
Closes gh-17915
1 parent c905ac3 commit 9a3ae4b

File tree

6 files changed

+262
-35
lines changed

6 files changed

+262
-35
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import java.util.LinkedHashMap;
2020

21+
import org.jspecify.annotations.Nullable;
22+
2123
import org.springframework.security.config.Customizer;
2224
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
2325
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -71,7 +73,7 @@ public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>>
7173

7274
private AccessDeniedHandler accessDeniedHandler;
7375

74-
private LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> defaultEntryPointMappings = new LinkedHashMap<>();
76+
private DelegatingAuthenticationEntryPoint.@Nullable Builder defaultEntryPoint;
7577

7678
private LinkedHashMap<RequestMatcher, AccessDeniedHandler> defaultDeniedHandlerMappings = new LinkedHashMap<>();
7779

@@ -161,7 +163,10 @@ public ExceptionHandlingConfigurer<H> authenticationEntryPoint(AuthenticationEnt
161163
*/
162164
public ExceptionHandlingConfigurer<H> defaultAuthenticationEntryPointFor(AuthenticationEntryPoint entryPoint,
163165
RequestMatcher preferredMatcher) {
164-
this.defaultEntryPointMappings.put(preferredMatcher, entryPoint);
166+
if (this.defaultEntryPoint == null) {
167+
this.defaultEntryPoint = DelegatingAuthenticationEntryPoint.builder();
168+
}
169+
this.defaultEntryPoint.addEntryPointFor(entryPoint, preferredMatcher);
165170
return this;
166171
}
167172

@@ -235,16 +240,10 @@ private AccessDeniedHandler createDefaultDeniedHandler(H http) {
235240
}
236241

237242
private AuthenticationEntryPoint createDefaultEntryPoint(H http) {
238-
if (this.defaultEntryPointMappings.isEmpty()) {
243+
if (this.defaultEntryPoint == null) {
239244
return new Http403ForbiddenEntryPoint();
240245
}
241-
if (this.defaultEntryPointMappings.size() == 1) {
242-
return this.defaultEntryPointMappings.values().iterator().next();
243-
}
244-
DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(
245-
this.defaultEntryPointMappings);
246-
entryPoint.setDefaultEntryPoint(this.defaultEntryPointMappings.values().iterator().next());
247-
return entryPoint;
246+
return this.defaultEntryPoint.build();
248247
}
249248

250249
/**

config/src/main/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurer.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import java.util.Arrays;
2020
import java.util.Collections;
21-
import java.util.LinkedHashMap;
2221

2322
import jakarta.servlet.http.HttpServletRequest;
2423

@@ -103,11 +102,12 @@ public final class HttpBasicConfigurer<B extends HttpSecurityBuilder<B>>
103102
*/
104103
public HttpBasicConfigurer() {
105104
realmName(DEFAULT_REALM);
106-
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints = new LinkedHashMap<>();
107-
entryPoints.put(X_REQUESTED_WITH, new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
108-
DelegatingAuthenticationEntryPoint defaultEntryPoint = new DelegatingAuthenticationEntryPoint(entryPoints);
109-
defaultEntryPoint.setDefaultEntryPoint(this.basicAuthEntryPoint);
110-
this.authenticationEntryPoint = defaultEntryPoint;
105+
// @formatter:off
106+
this.authenticationEntryPoint = DelegatingAuthenticationEntryPoint.builder()
107+
.addEntryPointFor(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), X_REQUESTED_WITH)
108+
.defaultEntryPoint(this.basicAuthEntryPoint)
109+
.build();
110+
// @formatter:on
111111
}
112112

113113
/**

config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import java.lang.reflect.Field;
2020
import java.util.Collections;
2121
import java.util.HashMap;
22-
import java.util.LinkedHashMap;
2322
import java.util.Map;
2423

2524
import jakarta.servlet.http.HttpServletRequest;
@@ -553,13 +552,15 @@ private AuthenticationEntryPoint getLoginEntryPoint(B http, String providerLogin
553552
RequestMatcher notXRequestedWith = new NegatedRequestMatcher(
554553
new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest"));
555554
RequestMatcher formLoginNotEnabled = getFormLoginNotEnabledRequestMatcher(http);
556-
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints = new LinkedHashMap<>();
557555
LoginUrlAuthenticationEntryPoint loginUrlEntryPoint = new LoginUrlAuthenticationEntryPoint(providerLoginPage);
558-
entryPoints.put(new AndRequestMatcher(notXRequestedWith, new NegatedRequestMatcher(defaultLoginPageMatcher),
559-
formLoginNotEnabled), loginUrlEntryPoint);
560-
DelegatingAuthenticationEntryPoint loginEntryPoint = new DelegatingAuthenticationEntryPoint(entryPoints);
561-
loginEntryPoint.setDefaultEntryPoint(this.getAuthenticationEntryPoint());
562-
return loginEntryPoint;
556+
RequestMatcher loginUrlMatcher = new AndRequestMatcher(notXRequestedWith,
557+
new NegatedRequestMatcher(defaultLoginPageMatcher), formLoginNotEnabled);
558+
// @formatter:off
559+
return DelegatingAuthenticationEntryPoint.builder()
560+
.addEntryPointFor(loginUrlEntryPoint, loginUrlMatcher)
561+
.defaultEntryPoint(getAuthenticationEntryPoint())
562+
.build();
563+
// @formatter:on
563564
}
564565

565566
private RequestMatcher getFormLoginNotEnabledRequestMatcher(B http) {

config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -339,13 +339,15 @@ private AuthenticationEntryPoint getLoginEntryPoint(B http, String providerLogin
339339
new OrRequestMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher);
340340
RequestMatcher notXRequestedWith = new NegatedRequestMatcher(
341341
new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest"));
342-
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints = new LinkedHashMap<>();
343342
LoginUrlAuthenticationEntryPoint loginUrlEntryPoint = new LoginUrlAuthenticationEntryPoint(providerLoginPage);
344-
entryPoints.put(new AndRequestMatcher(notXRequestedWith, new NegatedRequestMatcher(defaultLoginPageMatcher)),
345-
loginUrlEntryPoint);
346-
DelegatingAuthenticationEntryPoint loginEntryPoint = new DelegatingAuthenticationEntryPoint(entryPoints);
347-
loginEntryPoint.setDefaultEntryPoint(this.getAuthenticationEntryPoint());
348-
return loginEntryPoint;
343+
RequestMatcher loginUrlMatcher = new AndRequestMatcher(notXRequestedWith,
344+
new NegatedRequestMatcher(defaultLoginPageMatcher));
345+
// @formatter:off
346+
return DelegatingAuthenticationEntryPoint.builder()
347+
.addEntryPointFor(loginUrlEntryPoint, loginUrlMatcher)
348+
.defaultEntryPoint(getAuthenticationEntryPoint())
349+
.build();
350+
// @formatter:on
349351
}
350352

351353
private void setAuthenticationRequestRepository(B http,

web/src/main/java/org/springframework/security/web/authentication/DelegatingAuthenticationEntryPoint.java

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,18 @@
1717
package org.springframework.security.web.authentication;
1818

1919
import java.io.IOException;
20+
import java.util.ArrayList;
21+
import java.util.Arrays;
2022
import java.util.LinkedHashMap;
23+
import java.util.List;
24+
import java.util.stream.Collectors;
2125

2226
import jakarta.servlet.ServletException;
2327
import jakarta.servlet.http.HttpServletRequest;
2428
import jakarta.servlet.http.HttpServletResponse;
2529
import org.apache.commons.logging.Log;
2630
import org.apache.commons.logging.LogFactory;
31+
import org.jspecify.annotations.Nullable;
2732

2833
import org.springframework.beans.factory.InitializingBean;
2934
import org.springframework.core.log.LogMessage;
@@ -32,6 +37,7 @@
3237
import org.springframework.security.web.util.matcher.ELRequestMatcher;
3338
import org.springframework.security.web.util.matcher.RequestMatcher;
3439
import org.springframework.security.web.util.matcher.RequestMatcherEditor;
40+
import org.springframework.security.web.util.matcher.RequestMatcherEntry;
3541
import org.springframework.util.Assert;
3642

3743
/**
@@ -64,22 +70,63 @@ public class DelegatingAuthenticationEntryPoint implements AuthenticationEntryPo
6470

6571
private static final Log logger = LogFactory.getLog(DelegatingAuthenticationEntryPoint.class);
6672

67-
private final LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints;
73+
private final List<RequestMatcherEntry<AuthenticationEntryPoint>> entryPoints;
6874

6975
@SuppressWarnings("NullAway.Init")
7076
private AuthenticationEntryPoint defaultEntryPoint;
7177

72-
public DelegatingAuthenticationEntryPoint(LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints) {
78+
/**
79+
* Creates a new instance with the provided mappings.
80+
* @param entryPoints the mapping of {@link RequestMatcher} to
81+
* {@link AuthenticationEntryPoint}. Cannot be null or empty.
82+
* @param defaultEntryPoint the default {@link AuthenticationEntryPoint}. Cannot be
83+
* null.
84+
*/
85+
public DelegatingAuthenticationEntryPoint(AuthenticationEntryPoint defaultEntryPoint,
86+
RequestMatcherEntry<AuthenticationEntryPoint>... entryPoints) {
87+
Assert.notEmpty(entryPoints, "entryPoints cannot be empty");
88+
Assert.notNull(defaultEntryPoint, "defaultEntryPoint cannot be null");
89+
this.entryPoints = Arrays.asList(entryPoints);
90+
this.defaultEntryPoint = defaultEntryPoint;
91+
}
92+
93+
/**
94+
* Creates a new instance with the provided mappings.
95+
* @param defaultEntryPoint the default {@link AuthenticationEntryPoint}. Cannot be
96+
* null.
97+
* @param entryPoints the mapping of {@link RequestMatcher} to
98+
* {@link AuthenticationEntryPoint}. Cannot be null or empty.
99+
*/
100+
public DelegatingAuthenticationEntryPoint(AuthenticationEntryPoint defaultEntryPoint,
101+
List<RequestMatcherEntry<AuthenticationEntryPoint>> entryPoints) {
102+
Assert.notEmpty(entryPoints, "entryPoints cannot be empty");
103+
Assert.notNull(defaultEntryPoint, "defaultEntryPoint cannot be null");
73104
this.entryPoints = entryPoints;
105+
this.defaultEntryPoint = defaultEntryPoint;
106+
}
107+
108+
/**
109+
* Creates a new instance.
110+
* @param entryPoints
111+
* @deprecated Use
112+
* {@link #DelegatingAuthenticationEntryPoint(AuthenticationEntryPoint, List)}
113+
*/
114+
@Deprecated(forRemoval = true)
115+
public DelegatingAuthenticationEntryPoint(LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints) {
116+
this.entryPoints = entryPoints.entrySet()
117+
.stream()
118+
.map((e) -> new RequestMatcherEntry<>(e.getKey(), e.getValue()))
119+
.collect(Collectors.toList());
74120
}
75121

76122
@Override
77123
public void commence(HttpServletRequest request, HttpServletResponse response,
78124
AuthenticationException authException) throws IOException, ServletException {
79-
for (RequestMatcher requestMatcher : this.entryPoints.keySet()) {
125+
for (RequestMatcherEntry<AuthenticationEntryPoint> entry : this.entryPoints) {
126+
RequestMatcher requestMatcher = entry.getRequestMatcher();
80127
logger.debug(LogMessage.format("Trying to match using %s", requestMatcher));
81128
if (requestMatcher.matches(request)) {
82-
AuthenticationEntryPoint entryPoint = this.entryPoints.get(requestMatcher);
129+
AuthenticationEntryPoint entryPoint = entry.getEntry();
83130
logger.debug(LogMessage.format("Match found! Executing %s", entryPoint));
84131
entryPoint.commence(request, response, authException);
85132
return;
@@ -92,7 +139,10 @@ public void commence(HttpServletRequest request, HttpServletResponse response,
92139

93140
/**
94141
* EntryPoint which is used when no RequestMatcher returned true
142+
* @deprecated Use
143+
* {@link #DelegatingAuthenticationEntryPoint(AuthenticationEntryPoint, List)}
95144
*/
145+
@Deprecated(forRemoval = true)
96146
public void setDefaultEntryPoint(AuthenticationEntryPoint defaultEntryPoint) {
97147
this.defaultEntryPoint = defaultEntryPoint;
98148
}
@@ -103,4 +153,76 @@ public void afterPropertiesSet() {
103153
Assert.notNull(this.defaultEntryPoint, "defaultEntryPoint must be specified");
104154
}
105155

156+
/**
157+
* Creates a new {@link Builder}
158+
* @return the new {@link Builder}
159+
*/
160+
public static Builder builder() {
161+
return new Builder();
162+
}
163+
164+
/**
165+
* Used to build a new instance of {@link DelegatingAuthenticationEntryPoint}.
166+
*
167+
* @author Rob Winch
168+
* @since 7.0
169+
*/
170+
public static class Builder {
171+
172+
private @Nullable AuthenticationEntryPoint defaultEntryPoint;
173+
174+
private List<RequestMatcherEntry<AuthenticationEntryPoint>> entryPoints = new ArrayList<RequestMatcherEntry<AuthenticationEntryPoint>>();
175+
176+
/**
177+
* Set the default {@link AuthenticationEntryPoint} if none match. The default is
178+
* to use the first {@link AuthenticationEntryPoint} added in
179+
* {@link #addEntryPointFor(AuthenticationEntryPoint, RequestMatcher)}.
180+
* @param defaultEntryPoint the default {@link AuthenticationEntryPoint} to use.
181+
* @return the {@link Builder} for further customization.
182+
*/
183+
public Builder defaultEntryPoint(@Nullable AuthenticationEntryPoint defaultEntryPoint) {
184+
this.defaultEntryPoint = defaultEntryPoint;
185+
return this;
186+
}
187+
188+
/**
189+
* Adds an {@link AuthenticationEntryPoint} for the provided
190+
* {@link RequestMatcher}.
191+
* @param entryPoint the {@link AuthenticationEntryPoint} to use. Cannot be null.
192+
* @param requestMatcher the {@link RequestMatcher} to use. Cannot be null.
193+
* @return the {@link Builder} for further customization.
194+
*/
195+
public Builder addEntryPointFor(AuthenticationEntryPoint entryPoint, RequestMatcher requestMatcher) {
196+
Assert.notNull(entryPoint, "entryPoint cannot be null");
197+
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
198+
this.entryPoints.add(new RequestMatcherEntry<>(requestMatcher, entryPoint));
199+
return this;
200+
}
201+
202+
/**
203+
* Builds the {@link AuthenticationEntryPoint}. If the
204+
* {@link #defaultEntryPoint(AuthenticationEntryPoint)} is not set, then the first
205+
* {@link #addEntryPointFor(AuthenticationEntryPoint, RequestMatcher)} is used as
206+
* the default. If the {@link #defaultEntryPoint(AuthenticationEntryPoint)} is not
207+
* set and there is only a single
208+
* {@link #addEntryPointFor(AuthenticationEntryPoint, RequestMatcher)}, then the
209+
* {@link AuthenticationEntryPoint} is returned rather than wrapping it in
210+
* {@link DelegatingAuthenticationEntryPoint}.
211+
* @return the {@link AuthenticationEntryPoint} to use.
212+
*/
213+
public AuthenticationEntryPoint build() {
214+
Assert.notEmpty(this.entryPoints, "entryPoints cannot be empty");
215+
AuthenticationEntryPoint defaultEntryPoint = this.defaultEntryPoint;
216+
if (defaultEntryPoint == null) {
217+
AuthenticationEntryPoint firstAuthenticationEntryPoint = this.entryPoints.get(0).getEntry();
218+
if (this.entryPoints.size() == 1) {
219+
return firstAuthenticationEntryPoint;
220+
}
221+
defaultEntryPoint = firstAuthenticationEntryPoint;
222+
}
223+
return new DelegatingAuthenticationEntryPoint(defaultEntryPoint, this.entryPoints);
224+
}
225+
226+
}
227+
106228
}

0 commit comments

Comments
 (0)