Browse Source

SEC-2199: Support multiple AuthenticationEntryPoint defaults

Rob Winch 12 years ago
parent
commit
90bd241ce2

+ 10 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java

@@ -37,6 +37,8 @@ import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
+import org.springframework.web.accept.ContentNegotiationStrategy;
+import org.springframework.web.accept.HeaderContentNegotiationStrategy;
 
 /**
  * Provides a convenient base class for creating a {@link WebSecurityConfigurer}
@@ -51,6 +53,8 @@ public abstract class WebSecurityConfigurerAdapter implements SecurityConfigurer
 
     private ApplicationContext context;
 
+    private ContentNegotiationStrategy contentNegotiationStrategy = new HeaderContentNegotiationStrategy();
+
     private ObjectPostProcessor<Object> objectPostProcessor = new ObjectPostProcessor<Object>() {
         @Override
         public <T> T postProcess(T object) {
@@ -145,6 +149,7 @@ public abstract class WebSecurityConfigurerAdapter implements SecurityConfigurer
         authenticationBuilder.parentAuthenticationManager(authenticationManager);
         http = new HttpSecurity(objectPostProcessor,authenticationBuilder, parentAuthenticationBuilder.getSharedObjects());
         http.setSharedObject(UserDetailsService.class, userDetailsService());
+        http.setSharedObject(ContentNegotiationStrategy.class, contentNegotiationStrategy);
         if(!disableDefaults) {
             http
                 .exceptionHandling().and()
@@ -304,6 +309,11 @@ public abstract class WebSecurityConfigurerAdapter implements SecurityConfigurer
         this.context = context;
     }
 
+    @Autowired(required=false)
+    public void setContentNegotationStrategy(ContentNegotiationStrategy contentNegotiationStrategy) {
+        this.contentNegotiationStrategy = contentNegotiationStrategy;
+    }
+
     @Autowired(required=false)
     public void setObjectPostProcessor(ObjectPostProcessor<Object> objectPostProcessor) {
         this.objectPostProcessor = objectPostProcessor;

+ 20 - 2
config/src/main/java/org/springframework/security/config/annotation/web/configurers/AbstractAuthenticationFilterConfigurer.java

@@ -17,11 +17,11 @@ package org.springframework.security.config.annotation.web.configurers;
 
 import javax.servlet.http.HttpServletRequest;
 
+import org.springframework.http.MediaType;
 import org.springframework.security.authentication.AuthenticationDetailsSource;
 import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 import org.springframework.security.config.annotation.web.configurers.openid.OpenIDLoginConfigurer;
-import org.springframework.security.web.AuthenticationEntryPoint;
 import org.springframework.security.web.PortMapper;
 import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
 import org.springframework.security.web.authentication.AuthenticationFailureHandler;
@@ -33,6 +33,10 @@ import org.springframework.security.web.authentication.SimpleUrlAuthenticationFa
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
 import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
+import org.springframework.security.web.util.MediaTypeRequestMatcher;
+import org.springframework.security.web.util.RequestMatcher;
+import org.springframework.web.accept.ContentNegotiationStrategy;
+import org.springframework.web.accept.HeaderContentNegotiationStrategy;
 
 /**
  * Base class for confuring {@link AbstractAuthenticationFilterConfigurer}. This is intended for internal use only.
@@ -221,7 +225,21 @@ public abstract class AbstractAuthenticationFilterConfigurer<B  extends HttpSecu
         if(permitAll) {
             PermitAllSupport.permitAll(http, loginPage, loginProcessingUrl, failureUrl);
         }
-        http.setSharedObject(AuthenticationEntryPoint.class, postProcess(authenticationEntryPoint));
+        registerDefaultAuthenticationEntryPoint(http);
+    }
+
+    @SuppressWarnings("unchecked")
+    private void registerDefaultAuthenticationEntryPoint(B http) {
+        ExceptionHandlingConfigurer<B> exceptionHandling = http.getConfigurer(ExceptionHandlingConfigurer.class);
+        if(exceptionHandling == null) {
+            return;
+        }
+        ContentNegotiationStrategy contentNegotiationStrategy = http.getSharedObject(ContentNegotiationStrategy.class);
+        if(contentNegotiationStrategy == null) {
+            contentNegotiationStrategy = new HeaderContentNegotiationStrategy();
+        }
+        RequestMatcher preferredMatcher = new MediaTypeRequestMatcher(contentNegotiationStrategy, MediaType.APPLICATION_XHTML_XML, new MediaType("image","*"), MediaType.TEXT_HTML, MediaType.TEXT_PLAIN);
+        exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(authenticationEntryPoint), preferredMatcher);
     }
 
     @Override

+ 59 - 16
config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurer.java

@@ -15,15 +15,19 @@
  */
 package org.springframework.security.config.annotation.web.configurers;
 
+import java.util.LinkedHashMap;
+
 import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.web.AuthenticationEntryPoint;
 import org.springframework.security.web.access.AccessDeniedHandler;
 import org.springframework.security.web.access.AccessDeniedHandlerImpl;
 import org.springframework.security.web.access.ExceptionTranslationFilter;
+import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
 import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;
 import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
 import org.springframework.security.web.savedrequest.RequestCache;
+import org.springframework.security.web.util.RequestMatcher;
 
 /**
  * Adds exception handling for Spring Security related exceptions to an application. All properties have reasonable
@@ -47,8 +51,6 @@ import org.springframework.security.web.savedrequest.RequestCache;
  * The following shared objects are used:
  *
  * <ul>
- *     <li>{@link HttpSecurity#authenticationEntryPoint()} is used to process requests that require
- *     authentication</li>
  *     <li>If no explicit {@link RequestCache}, is provided a {@link RequestCache} shared object is used to replay
  *     the request after authentication is successful</li>
  *     <li>{@link AuthenticationEntryPoint} - see {@link #authenticationEntryPoint(AuthenticationEntryPoint)} </li>
@@ -63,6 +65,8 @@ public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>>
 
     private AccessDeniedHandler accessDeniedHandler;
 
+    private LinkedHashMap<RequestMatcher,AuthenticationEntryPoint> defaultEntryPointMappings = new LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>();
+
     /**
      * Creates a new instance
      * @see HttpSecurity#exceptionHandling()
@@ -96,18 +100,51 @@ public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>>
     }
 
     /**
-     * Sets the {@link AuthenticationEntryPoint} to be used. Defaults to the
-     * {@link HttpSecurity#getSharedObject(Class)} value. If that is not
-     * provided defaults to {@link Http403ForbiddenEntryPoint}.
+     * Sets the {@link AuthenticationEntryPoint} to be used.
+     *
+     * <p>
+     * If no {@link #authenticationEntryPoint(AuthenticationEntryPoint)} is
+     * specified, then
+     * {@link #defaultAuthenticationEntryPointFor(AuthenticationEntryPoint, RequestMatcher)}
+     * will be used. The first {@link AuthenticationEntryPoint} will be used as
+     * the default is no matches were found.
+     * </p>
+     *
+     * <p>
+     * If that is not provided defaults to {@link Http403ForbiddenEntryPoint}.
+     * </p>
      *
-     * @param authenticationEntryPoint the {@link AuthenticationEntryPoint} to use
-     * @return the {@link ExceptionHandlingConfigurer} for further customizations
+     * @param authenticationEntryPoint
+     *            the {@link AuthenticationEntryPoint} to use
+     * @return the {@link ExceptionHandlingConfigurer} for further
+     *         customizations
      */
     public ExceptionHandlingConfigurer<H> authenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
         this.authenticationEntryPoint = authenticationEntryPoint;
         return this;
     }
 
+    /**
+     * Sets a default {@link AuthenticationEntryPoint} to be used which prefers
+     * being invoked for the provided {@link RequestMatcher}. If only a single
+     * default {@link AuthenticationEntryPoint} is specified, it will be what is
+     * used for the default {@link AuthenticationEntryPoint}. If multiple
+     * default {@link AuthenticationEntryPoint} instances are configured, then a
+     * {@link DelegatingAuthenticationEntryPoint} will be used.
+     *
+     * @param entryPoint
+     *            the {@link AuthenticationEntryPoint} to use
+     * @param preferredMatcher
+     *            the {@link RequestMatcher} for this default
+     *            {@link AuthenticationEntryPoint}
+     * @return the {@link ExceptionHandlingConfigurer} for further
+     *         customizations
+     */
+    public ExceptionHandlingConfigurer<H> defaultAuthenticationEntryPointFor(AuthenticationEntryPoint entryPoint, RequestMatcher preferredMatcher) {
+        this.defaultEntryPointMappings.put(preferredMatcher, entryPoint);
+        return this;
+    }
+
     /**
      * Gets any explicitly configured {@link AuthenticationEntryPoint}
      * @return
@@ -118,7 +155,7 @@ public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>>
 
     @Override
     public void configure(H http) throws Exception {
-        AuthenticationEntryPoint entryPoint = getEntryPoint(http);
+        AuthenticationEntryPoint entryPoint = getAuthenticationEntryPoint(http);
         ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(entryPoint, getRequestCache(http));
         if(accessDeniedHandler != null) {
             exceptionTranslationFilter.setAccessDeniedHandler(accessDeniedHandler);
@@ -126,22 +163,28 @@ public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>>
         exceptionTranslationFilter = postProcess(exceptionTranslationFilter);
         http.addFilter(exceptionTranslationFilter);
     }
-
     /**
      * Gets the {@link AuthenticationEntryPoint} according to the rules specified by {@link #authenticationEntryPoint(AuthenticationEntryPoint)}
      * @param http the {@link HttpSecurity} used to look up shared {@link AuthenticationEntryPoint}
      * @return the {@link AuthenticationEntryPoint} to use
      */
-    AuthenticationEntryPoint getEntryPoint(H http) {
+     AuthenticationEntryPoint getAuthenticationEntryPoint(H http) {
         AuthenticationEntryPoint entryPoint = this.authenticationEntryPoint;
         if(entryPoint == null) {
-            AuthenticationEntryPoint sharedEntryPoint = http.getSharedObject(AuthenticationEntryPoint.class);
-            if(sharedEntryPoint != null) {
-                entryPoint = sharedEntryPoint;
-            } else {
-                entryPoint = new Http403ForbiddenEntryPoint();
-            }
+            entryPoint = createDefaultEntryPoint(http);
+        }
+        return entryPoint;
+    }
+
+    private AuthenticationEntryPoint createDefaultEntryPoint(H http) {
+        if(defaultEntryPointMappings.isEmpty()) {
+            return new Http403ForbiddenEntryPoint();
+        }
+        if(defaultEntryPointMappings.size() == 1) {
+            return defaultEntryPointMappings.values().iterator().next();
         }
+        DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(defaultEntryPointMappings);
+        entryPoint.setDefaultEntryPoint(defaultEntryPointMappings.values().iterator().next());
         return entryPoint;
     }
 

+ 23 - 2
config/src/main/java/org/springframework/security/config/annotation/web/configurers/HttpBasicConfigurer.java

@@ -15,8 +15,11 @@
  */
 package org.springframework.security.config.annotation.web.configurers;
 
+import java.util.Collections;
+
 import javax.servlet.http.HttpServletRequest;
 
+import org.springframework.http.MediaType;
 import org.springframework.security.authentication.AuthenticationDetailsSource;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
@@ -25,6 +28,10 @@ import org.springframework.security.web.AuthenticationEntryPoint;
 import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
 import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
 import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
+import org.springframework.security.web.util.MediaTypeRequestMatcher;
+import org.springframework.security.web.util.RequestMatcher;
+import org.springframework.web.accept.ContentNegotiationStrategy;
+import org.springframework.web.accept.HeaderContentNegotiationStrategy;
 
 /**
  * Adds HTTP basic based authentication. All attributes have reasonable defaults
@@ -118,8 +125,22 @@ public final class HttpBasicConfigurer<B extends HttpSecurityBuilder<B>> extends
     }
 
     public void init(B http) throws Exception {
-        http
-            .setSharedObject(AuthenticationEntryPoint.class, authenticationEntryPoint);
+        registerDefaultAuthenticationEntryPoint(http);
+    }
+
+    @SuppressWarnings("unchecked")
+    private void registerDefaultAuthenticationEntryPoint(B http) {
+        ExceptionHandlingConfigurer<B> exceptionHandling = http.getConfigurer(ExceptionHandlingConfigurer.class);
+        if(exceptionHandling == null) {
+            return;
+        }
+        ContentNegotiationStrategy contentNegotiationStrategy = http.getSharedObject(ContentNegotiationStrategy.class);
+        if(contentNegotiationStrategy == null) {
+            contentNegotiationStrategy = new HeaderContentNegotiationStrategy();
+        }
+        MediaTypeRequestMatcher preferredMatcher = new MediaTypeRequestMatcher(contentNegotiationStrategy, MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_FORM_URLENCODED,  MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_XML, MediaType.MULTIPART_FORM_DATA, MediaType.TEXT_XML);
+        preferredMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
+        exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(authenticationEntryPoint), preferredMatcher);
     }
 
     @Override

+ 1 - 1
config/src/main/java/org/springframework/security/config/annotation/web/configurers/ServletApiConfigurer.java

@@ -75,7 +75,7 @@ public final class ServletApiConfigurer<H extends HttpSecurityBuilder<H>> extend
     public void configure(H http) throws Exception {
         securityContextRequestFilter.setAuthenticationManager(http.getAuthenticationManager());
         ExceptionHandlingConfigurer<H> exceptionConf = http.getConfigurer(ExceptionHandlingConfigurer.class);
-        AuthenticationEntryPoint authenticationEntryPoint = exceptionConf == null ? null : exceptionConf.getEntryPoint(http);
+        AuthenticationEntryPoint authenticationEntryPoint = exceptionConf == null ? null : exceptionConf.getAuthenticationEntryPoint(http);
         securityContextRequestFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
         LogoutConfigurer<H> logoutConf = http.getConfigurer(LogoutConfigurer.class);
         List<LogoutHandler> logoutHandlers = logoutConf == null ? null : logoutConf.getLogoutHandlers();

+ 33 - 0
config/src/test/groovy/org/springframework/security/config/annotation/web/WebSecurityConfigurerAdapterTests.groovy

@@ -40,6 +40,8 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
 import org.springframework.security.core.Authentication
 import org.springframework.security.core.authority.AuthorityUtils
 import org.springframework.security.ldap.DefaultSpringSecurityContextSource
+import org.springframework.web.accept.ContentNegotiationStrategy;
+import org.springframework.web.accept.HeaderContentNegotiationStrategy;
 
 /**
  * @author Rob Winch
@@ -99,4 +101,35 @@ class WebSecurityConfigurerAdapterTests extends BaseSpringSpec {
             EVENTS.add(e)
         }
     }
+
+    def "Override ContentNegotiationStrategy with @Bean"() {
+        setup:
+            OverrideContentNegotiationStrategySharedObjectConfig.CNS = Mock(ContentNegotiationStrategy)
+        when:
+            loadConfig(OverrideContentNegotiationStrategySharedObjectConfig)
+        then:
+            context.getBean(OverrideContentNegotiationStrategySharedObjectConfig).http.getSharedObject(ContentNegotiationStrategy) == OverrideContentNegotiationStrategySharedObjectConfig.CNS
+    }
+
+    @EnableWebSecurity
+    @Configuration
+    static class OverrideContentNegotiationStrategySharedObjectConfig extends WebSecurityConfigurerAdapter {
+        static ContentNegotiationStrategy CNS
+
+        @Bean
+        public ContentNegotiationStrategy cns() {
+            return CNS
+        }
+    }
+
+    def "ContentNegotiationStrategy shareObject defaults to Header with no @Bean"() {
+        when:
+            loadConfig(ContentNegotiationStrategyDefaultSharedObjectConfig)
+        then:
+            context.getBean(ContentNegotiationStrategyDefaultSharedObjectConfig).http.getSharedObject(ContentNegotiationStrategy).class == HeaderContentNegotiationStrategy
+    }
+
+    @EnableWebSecurity
+    @Configuration
+    static class ContentNegotiationStrategyDefaultSharedObjectConfig extends WebSecurityConfigurerAdapter {}
 }

+ 4 - 0
config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/DefaultLoginPageConfigurerTests.groovy

@@ -29,6 +29,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.BaseWebConfig;
 import org.springframework.security.config.annotation.web.configurers.DefaultLoginPageConfigurer;
 import org.springframework.security.web.FilterChainProxy
+import org.springframework.security.web.access.ExceptionTranslationFilter;
 import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
 import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler
@@ -331,6 +332,8 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
             http
                 // must set builder manually due to groovy not selecting correct method
                 .apply(defaultLoginConfig).and()
+                .exceptionHandling()
+                    .and()
                 .formLogin()
                     .and()
                 .build()
@@ -339,5 +342,6 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
             1 * objectPostProcessor.postProcess(_ as DefaultLoginPageViewFilter) >> {DefaultLoginPageViewFilter o -> o}
             1 * objectPostProcessor.postProcess(_ as UsernamePasswordAuthenticationFilter) >> {UsernamePasswordAuthenticationFilter o -> o}
             1 * objectPostProcessor.postProcess(_ as LoginUrlAuthenticationEntryPoint) >> {LoginUrlAuthenticationEntryPoint o -> o}
+            1 * objectPostProcessor.postProcess(_ as ExceptionTranslationFilter) >> {ExceptionTranslationFilter o -> o}
     }
 }

+ 140 - 2
config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/ExceptionHandlingConfigurerTests.groovy

@@ -15,11 +15,29 @@
  */
 package org.springframework.security.config.annotation.web.configurers
 
+import javax.servlet.http.HttpServletResponse
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration
+import org.springframework.http.MediaType
+import org.springframework.mock.web.MockFilterChain
+import org.springframework.mock.web.MockHttpServletRequest
+import org.springframework.mock.web.MockHttpServletResponse
 import org.springframework.security.config.annotation.AnyObjectPostProcessor
 import org.springframework.security.config.annotation.BaseSpringSpec
-import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
-import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
+import org.springframework.security.web.AuthenticationEntryPoint;
 import org.springframework.security.web.access.ExceptionTranslationFilter
+import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint
+import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
+import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
+import org.springframework.web.accept.ContentNegotiationStrategy;
+import org.springframework.web.accept.HeaderContentNegotiationStrategy;
+
+import spock.lang.Unroll
 
 /**
  *
@@ -40,4 +58,124 @@ class ExceptionHandlingConfigurerTests extends BaseSpringSpec {
         then: "ExceptionTranslationFilter is registered with LifecycleManager"
             1 * opp.postProcess(_ as ExceptionTranslationFilter) >> {ExceptionTranslationFilter o -> o}
     }
+
+    @Unroll
+    def "SEC-2199: defaultEntryPoint for httpBasic and formLogin"(String acceptHeader, int httpStatus) {
+        setup:
+            loadConfig(HttpBasicAndFormLoginEntryPointsConfig)
+        when:
+            request.addHeader("Accept", acceptHeader)
+            springSecurityFilterChain.doFilter(request,response,chain)
+        then:
+            response.status == httpStatus
+        where:
+            acceptHeader                                 | httpStatus
+            MediaType.ALL_VALUE                          | HttpServletResponse.SC_MOVED_TEMPORARILY
+            MediaType.APPLICATION_XHTML_XML_VALUE        | HttpServletResponse.SC_MOVED_TEMPORARILY
+            MediaType.IMAGE_GIF_VALUE                    | HttpServletResponse.SC_MOVED_TEMPORARILY
+            MediaType.IMAGE_JPEG_VALUE                   | HttpServletResponse.SC_MOVED_TEMPORARILY
+            MediaType.IMAGE_PNG_VALUE                    | HttpServletResponse.SC_MOVED_TEMPORARILY
+            MediaType.TEXT_HTML_VALUE                    | HttpServletResponse.SC_MOVED_TEMPORARILY
+            MediaType.TEXT_PLAIN_VALUE                   | HttpServletResponse.SC_MOVED_TEMPORARILY
+            MediaType.APPLICATION_ATOM_XML_VALUE         | HttpServletResponse.SC_UNAUTHORIZED
+            MediaType.APPLICATION_FORM_URLENCODED_VALUE  | HttpServletResponse.SC_UNAUTHORIZED
+            MediaType.APPLICATION_JSON_VALUE             | HttpServletResponse.SC_UNAUTHORIZED
+            MediaType.APPLICATION_OCTET_STREAM_VALUE     | HttpServletResponse.SC_UNAUTHORIZED
+            MediaType.APPLICATION_XML_VALUE              | HttpServletResponse.SC_UNAUTHORIZED
+            MediaType.MULTIPART_FORM_DATA_VALUE          | HttpServletResponse.SC_UNAUTHORIZED
+            MediaType.TEXT_XML_VALUE                     | HttpServletResponse.SC_UNAUTHORIZED
+    }
+
+    def "ContentNegotiationStrategy defaults to HeaderContentNegotiationStrategy"() {
+        when:
+            loadConfig(HttpBasicAndFormLoginEntryPointsConfig)
+            DelegatingAuthenticationEntryPoint delegateEntryPoint = findFilter(ExceptionTranslationFilter).authenticationEntryPoint
+        then:
+            delegateEntryPoint.entryPoints.keySet().collect {it.contentNegotiationStrategy.class} == [HeaderContentNegotiationStrategy,HeaderContentNegotiationStrategy]
+    }
+
+    @EnableWebSecurity
+    @Configuration
+    static class HttpBasicAndFormLoginEntryPointsConfig extends WebSecurityConfigurerAdapter {
+
+        @Override
+        protected void registerAuthentication(AuthenticationManagerBuilder auth)
+                throws Exception {
+            auth
+                .inMemoryAuthentication()
+                    .withUser("user").password("password").roles("USER")
+        }
+
+        @Override
+        protected void configure(HttpSecurity http) throws Exception {
+            http
+                .authorizeUrls()
+                    .anyRequest().authenticated()
+                    .and()
+                .httpBasic()
+                    .and()
+                .formLogin()
+        }
+    }
+
+    def "ContentNegotiationStrategy overrides with @Bean"() {
+        setup:
+            OverrideContentNegotiationStrategySharedObjectConfig.CNS = Mock(ContentNegotiationStrategy)
+        when:
+            loadConfig(OverrideContentNegotiationStrategySharedObjectConfig)
+            DelegatingAuthenticationEntryPoint delegateEntryPoint = findFilter(ExceptionTranslationFilter).authenticationEntryPoint
+        then:
+            delegateEntryPoint.entryPoints.keySet().collect {it.contentNegotiationStrategy} == [OverrideContentNegotiationStrategySharedObjectConfig.CNS,OverrideContentNegotiationStrategySharedObjectConfig.CNS]
+    }
+
+    def "Override ContentNegotiationStrategy with @Bean"() {
+        setup:
+            OverrideContentNegotiationStrategySharedObjectConfig.CNS = Mock(ContentNegotiationStrategy)
+        when:
+            loadConfig(OverrideContentNegotiationStrategySharedObjectConfig)
+        then:
+            context.getBean(OverrideContentNegotiationStrategySharedObjectConfig).http.getSharedObject(ContentNegotiationStrategy) == OverrideContentNegotiationStrategySharedObjectConfig.CNS
+    }
+
+    @EnableWebSecurity
+    @Configuration
+    static class OverrideContentNegotiationStrategySharedObjectConfig extends WebSecurityConfigurerAdapter {
+        static ContentNegotiationStrategy CNS
+
+        @Bean
+        public ContentNegotiationStrategy cns() {
+            return CNS
+        }
+    }
+
+    def "delegatingAuthenticationEntryPoint.defaultEntryPoint is LoginUrlAuthenticationEntryPoint when using DefaultHttpConf"() {
+        when:
+            loadConfig(DefaultHttpConf)
+        then:
+            findFilter(ExceptionTranslationFilter).authenticationEntryPoint.defaultEntryPoint.class == LoginUrlAuthenticationEntryPoint
+    }
+
+    @EnableWebSecurity
+    @Configuration
+    static class DefaultHttpConf extends WebSecurityConfigurerAdapter {
+    }
+
+    def "delegatingAuthenticationEntryPoint.defaultEntryPoint is BasicAuthenticationEntryPoint when httpBasic before formLogin"() {
+        when:
+            loadConfig(BasicAuthenticationEntryPointBeforeFormLoginConf)
+        then:
+            findFilter(ExceptionTranslationFilter).authenticationEntryPoint.defaultEntryPoint.class == BasicAuthenticationEntryPoint
+    }
+
+    @EnableWebSecurity
+    @Configuration
+    static class BasicAuthenticationEntryPointBeforeFormLoginConf extends WebSecurityConfigurerAdapter {
+        @Override
+        protected void configure(HttpSecurity http) throws Exception {
+            http
+                .httpBasic()
+                    .and()
+                .formLogin()
+        }
+    }
 }

+ 6 - 2
config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurerTests.groovy

@@ -202,13 +202,17 @@ class FormLoginConfigurerTests extends BaseSpringSpec {
             HttpSecurity http = new HttpSecurity(opp, authenticationBldr, [:])
         when:
             http
+                .exceptionHandling()
+                    .and()
                 .formLogin()
                     .and()
                 .build()
 
-        then: "UsernamePasswordAuthenticationFilter is registered with LifecycleManager"
+        then: "UsernamePasswordAuthenticationFilter is registered with ObjectPostProcessor"
             1 * opp.postProcess(_ as UsernamePasswordAuthenticationFilter) >> {UsernamePasswordAuthenticationFilter o -> o}
-        and: "LoginUrlAuthenticationEntryPoint is registered with LifecycleManager"
+        and: "LoginUrlAuthenticationEntryPoint is registered with ObjectPostProcessor"
             1 * opp.postProcess(_ as LoginUrlAuthenticationEntryPoint) >> {LoginUrlAuthenticationEntryPoint o -> o}
+        and: "ExceptionTranslationFilter is registered with ObjectPostProcessor"
+            1 * opp.postProcess(_ as ExceptionTranslationFilter) >> {ExceptionTranslationFilter o -> o}
     }
 }