HTTP Basic default logout ignores text/html

This fixes an issue where Chrome sends an accept header of application/xml
which triggers an HTTP 204 to be returned

Fixes gh-3902
This commit is contained in:
Rob Winch 2016-06-14 16:14:41 -05:00
parent e7fd6f6c3f
commit 9e3d2e2d99
3 changed files with 43 additions and 14 deletions

View File

@ -15,6 +15,7 @@
*/ */
package org.springframework.security.config.annotation.web.configurers; package org.springframework.security.config.annotation.web.configurers;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -32,10 +33,11 @@ import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.RememberMeServices; import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler; import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher; import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.accept.ContentNegotiationStrategy; import org.springframework.web.accept.ContentNegotiationStrategy;
@ -96,8 +98,8 @@ public final class HttpBasicConfigurer<B extends HttpSecurityBuilder<B>> extends
DelegatingAuthenticationEntryPoint defaultEntryPoint = new DelegatingAuthenticationEntryPoint( DelegatingAuthenticationEntryPoint defaultEntryPoint = new DelegatingAuthenticationEntryPoint(
entryPoints); entryPoints);
defaultEntryPoint.setDefaultEntryPoint(basicAuthEntryPoint); defaultEntryPoint.setDefaultEntryPoint(this.basicAuthEntryPoint);
authenticationEntryPoint = defaultEntryPoint; this.authenticationEntryPoint = defaultEntryPoint;
} }
/** /**
@ -110,8 +112,8 @@ public final class HttpBasicConfigurer<B extends HttpSecurityBuilder<B>> extends
* @throws Exception * @throws Exception
*/ */
public HttpBasicConfigurer<B> realmName(String realmName) throws Exception { public HttpBasicConfigurer<B> realmName(String realmName) throws Exception {
basicAuthEntryPoint.setRealmName(realmName); this.basicAuthEntryPoint.setRealmName(realmName);
basicAuthEntryPoint.afterPropertiesSet(); this.basicAuthEntryPoint.afterPropertiesSet();
return this; return this;
} }
@ -144,23 +146,29 @@ public final class HttpBasicConfigurer<B extends HttpSecurityBuilder<B>> extends
return this; return this;
} }
@Override
public void init(B http) throws Exception { public void init(B http) throws Exception {
registerDefaults(http); registerDefaults(http);
} }
@SuppressWarnings("unchecked")
private void registerDefaults(B http) { private void registerDefaults(B http) {
ContentNegotiationStrategy contentNegotiationStrategy = http ContentNegotiationStrategy contentNegotiationStrategy = http
.getSharedObject(ContentNegotiationStrategy.class); .getSharedObject(ContentNegotiationStrategy.class);
if (contentNegotiationStrategy == null) { if (contentNegotiationStrategy == null) {
contentNegotiationStrategy = new HeaderContentNegotiationStrategy(); contentNegotiationStrategy = new HeaderContentNegotiationStrategy();
} }
MediaTypeRequestMatcher preferredMatcher = new MediaTypeRequestMatcher( MediaTypeRequestMatcher restMatcher = new MediaTypeRequestMatcher(
contentNegotiationStrategy, MediaType.APPLICATION_ATOM_XML, contentNegotiationStrategy, MediaType.APPLICATION_ATOM_XML,
MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON, MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON,
MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_XML, MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_XML,
MediaType.MULTIPART_FORM_DATA, MediaType.TEXT_XML); MediaType.MULTIPART_FORM_DATA, MediaType.TEXT_XML);
preferredMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); restMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
RequestMatcher notHtmlMatcher = new NegatedRequestMatcher(
new MediaTypeRequestMatcher(contentNegotiationStrategy,
MediaType.TEXT_HTML));
RequestMatcher preferredMatcher = new AndRequestMatcher(
Arrays.<RequestMatcher>asList(notHtmlMatcher, restMatcher));
registerDefaultEntryPoint(http, preferredMatcher); registerDefaultEntryPoint(http, preferredMatcher);
registerDefaultLogoutSuccessHandler(http, preferredMatcher); registerDefaultLogoutSuccessHandler(http, preferredMatcher);
@ -173,7 +181,7 @@ public final class HttpBasicConfigurer<B extends HttpSecurityBuilder<B>> extends
return; return;
} }
exceptionHandling.defaultAuthenticationEntryPointFor( exceptionHandling.defaultAuthenticationEntryPointFor(
postProcess(authenticationEntryPoint), preferredMatcher); postProcess(this.authenticationEntryPoint), preferredMatcher);
} }
private void registerDefaultLogoutSuccessHandler(B http, RequestMatcher preferredMatcher) { private void registerDefaultLogoutSuccessHandler(B http, RequestMatcher preferredMatcher) {
@ -191,10 +199,10 @@ public final class HttpBasicConfigurer<B extends HttpSecurityBuilder<B>> extends
AuthenticationManager authenticationManager = http AuthenticationManager authenticationManager = http
.getSharedObject(AuthenticationManager.class); .getSharedObject(AuthenticationManager.class);
BasicAuthenticationFilter basicAuthenticationFilter = new BasicAuthenticationFilter( BasicAuthenticationFilter basicAuthenticationFilter = new BasicAuthenticationFilter(
authenticationManager, authenticationEntryPoint); authenticationManager, this.authenticationEntryPoint);
if (authenticationDetailsSource != null) { if (this.authenticationDetailsSource != null) {
basicAuthenticationFilter basicAuthenticationFilter
.setAuthenticationDetailsSource(authenticationDetailsSource); .setAuthenticationDetailsSource(this.authenticationDetailsSource);
} }
RememberMeServices rememberMeServices = http.getSharedObject(RememberMeServices.class); RememberMeServices rememberMeServices = http.getSharedObject(RememberMeServices.class);
if(rememberMeServices != null) { if(rememberMeServices != null) {

View File

@ -91,7 +91,9 @@ class ExceptionHandlingConfigurerTests extends BaseSpringSpec {
loadConfig(HttpBasicAndFormLoginEntryPointsConfig) loadConfig(HttpBasicAndFormLoginEntryPointsConfig)
DelegatingAuthenticationEntryPoint delegateEntryPoint = findFilter(ExceptionTranslationFilter).authenticationEntryPoint DelegatingAuthenticationEntryPoint delegateEntryPoint = findFilter(ExceptionTranslationFilter).authenticationEntryPoint
then: then:
delegateEntryPoint.entryPoints.keySet().collect {it.contentNegotiationStrategy.class} == [HeaderContentNegotiationStrategy,HeaderContentNegotiationStrategy] def entryPoints = delegateEntryPoint.entryPoints.keySet() as List
entryPoints[0].requestMatchers[1].contentNegotiationStrategy.class == HeaderContentNegotiationStrategy
entryPoints[1].contentNegotiationStrategy.class == HeaderContentNegotiationStrategy
} }
@EnableWebSecurity @EnableWebSecurity
@ -123,7 +125,9 @@ class ExceptionHandlingConfigurerTests extends BaseSpringSpec {
loadConfig(OverrideContentNegotiationStrategySharedObjectConfig) loadConfig(OverrideContentNegotiationStrategySharedObjectConfig)
DelegatingAuthenticationEntryPoint delegateEntryPoint = findFilter(ExceptionTranslationFilter).authenticationEntryPoint DelegatingAuthenticationEntryPoint delegateEntryPoint = findFilter(ExceptionTranslationFilter).authenticationEntryPoint
then: then:
delegateEntryPoint.entryPoints.keySet().collect {it.contentNegotiationStrategy} == [OverrideContentNegotiationStrategySharedObjectConfig.CNS,OverrideContentNegotiationStrategySharedObjectConfig.CNS] def entryPoints = delegateEntryPoint.entryPoints.keySet() as List
entryPoints[0].contentNegotiationStrategy == OverrideContentNegotiationStrategySharedObjectConfig.CNS
entryPoints[1].requestMatchers[1].contentNegotiationStrategy == OverrideContentNegotiationStrategySharedObjectConfig.CNS
} }
def "Override ContentNegotiationStrategy with @Bean"() { def "Override ContentNegotiationStrategy with @Bean"() {

View File

@ -197,4 +197,21 @@ class LogoutConfigurerTests extends BaseSpringSpec {
@EnableWebSecurity @EnableWebSecurity
static class LogoutHandlerContentNegotiation extends WebSecurityConfigurerAdapter { static class LogoutHandlerContentNegotiation extends WebSecurityConfigurerAdapter {
} }
// gh-3902
def "logout in chrome is 302"() {
setup:
loadConfig(LogoutHandlerContentNegotiationForChrome)
when:
login()
request.method = 'POST'
request.servletPath = '/logout'
request.addHeader('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8')
springSecurityFilterChain.doFilter(request,response,chain)
then:
response.status == 302
}
@EnableWebSecurity
static class LogoutHandlerContentNegotiationForChrome extends WebSecurityConfigurerAdapter {
}
} }