From 510cd599809ecf75143de91c4ec15d8025b33dd0 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 19 Apr 2016 13:57:51 -0500 Subject: [PATCH] Default logout negotiation in Java Configuration This commit adds content negotiation for log out. Fixes gh-3282 --- .../web/configurers/HttpBasicConfigurer.java | 31 ++++++++--- .../web/configurers/LogoutConfigurer.java | 47 +++++++++++++++-- .../configurers/LogoutConfigurerTests.groovy | 52 +++++++++++++++++++ 3 files changed, 118 insertions(+), 12 deletions(-) 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 78d14308a5..382434c7be 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 @@ -31,6 +31,8 @@ import org.springframework.security.web.authentication.DelegatingAuthenticationE import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.authentication.RememberMeServices; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +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.BasicAuthenticationFilter; import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; @@ -143,16 +145,11 @@ public final class HttpBasicConfigurer> extends } public void init(B http) throws Exception { - registerDefaultAuthenticationEntryPoint(http); + registerDefaults(http); } @SuppressWarnings("unchecked") - private void registerDefaultAuthenticationEntryPoint(B http) { - ExceptionHandlingConfigurer exceptionHandling = http - .getConfigurer(ExceptionHandlingConfigurer.class); - if (exceptionHandling == null) { - return; - } + private void registerDefaults(B http) { ContentNegotiationStrategy contentNegotiationStrategy = http .getSharedObject(ContentNegotiationStrategy.class); if (contentNegotiationStrategy == null) { @@ -164,9 +161,29 @@ public final class HttpBasicConfigurer> extends MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_XML, MediaType.MULTIPART_FORM_DATA, MediaType.TEXT_XML); preferredMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); + + registerDefaultEntryPoint(http, preferredMatcher); + registerDefaultLogoutSuccessHandler(http, preferredMatcher); + } + + private void registerDefaultEntryPoint(B http, RequestMatcher preferredMatcher) { + ExceptionHandlingConfigurer exceptionHandling = http + .getConfigurer(ExceptionHandlingConfigurer.class); + if (exceptionHandling == null) { + return; + } exceptionHandling.defaultAuthenticationEntryPointFor( postProcess(authenticationEntryPoint), preferredMatcher); + } + private void registerDefaultLogoutSuccessHandler(B http, RequestMatcher preferredMatcher) { + LogoutConfigurer logout = http + .getConfigurer(LogoutConfigurer.class); + if (logout == null) { + return; + } + LogoutConfigurer handler = logout.defaultLogoutSuccessHandlerFor( + postProcess(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.NO_CONTENT)), preferredMatcher); } @Override diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java index a99f883bee..96327bf801 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java @@ -16,6 +16,7 @@ package org.springframework.security.config.annotation.web.configurers; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import javax.servlet.http.HttpSession; @@ -24,7 +25,9 @@ import org.springframework.security.config.annotation.SecurityConfigurer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint; import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler; +import org.springframework.security.web.authentication.logout.DelegatingLogoutSuccessHandler; import org.springframework.security.web.authentication.logout.LogoutFilter; import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; @@ -71,6 +74,9 @@ public final class LogoutConfigurer> extends private boolean permitAll; private boolean customLogoutSuccess; + private LinkedHashMap defaultLogoutSuccessHandlerMappings = + new LinkedHashMap(); + /** * Creates a new instance * @see HttpSecurity#logout() @@ -205,6 +211,27 @@ public final class LogoutConfigurer> extends return this; } + /** + * Sets a default {@link LogoutSuccessHandler} to be used which prefers being invoked + * for the provided {@link RequestMatcher}. If no {@link LogoutSuccessHandler} is + * specified a {@link SimpleUrlLogoutSuccessHandler} will be used. + * If any default {@link LogoutSuccessHandler} instances are configured, then a + * {@link DelegatingLogoutSuccessHandler} will be used that defaults to a + * {@link SimpleUrlLogoutSuccessHandler}. + * + * @param handler the {@link LogoutSuccessHandler} to use + * @param preferredMatcher the {@link RequestMatcher} for this default + * {@link LogoutSuccessHandler} + * @return the {@link LogoutConfigurer} for further customizations + */ + public LogoutConfigurer defaultLogoutSuccessHandlerFor( + LogoutSuccessHandler handler, RequestMatcher preferredMatcher) { + Assert.notNull(handler, "handler cannot be null"); + Assert.notNull(preferredMatcher, "preferredMatcher cannot be null"); + this.defaultLogoutSuccessHandlerMappings.put(preferredMatcher, handler); + return this; + } + /** * Grants access to the {@link #logoutSuccessUrl(String)} and the * {@link #logoutUrl(String)} for every user. @@ -224,12 +251,22 @@ public final class LogoutConfigurer> extends * @return the {@link LogoutSuccessHandler} to use */ private LogoutSuccessHandler getLogoutSuccessHandler() { - if (logoutSuccessHandler != null) { - return logoutSuccessHandler; + LogoutSuccessHandler handler = this.logoutSuccessHandler; + if (handler == null) { + handler = createDefaultSuccessHandler(); } - SimpleUrlLogoutSuccessHandler logoutSuccessHandler = new SimpleUrlLogoutSuccessHandler(); - logoutSuccessHandler.setDefaultTargetUrl(logoutSuccessUrl); - return logoutSuccessHandler; + return handler; + } + + private LogoutSuccessHandler createDefaultSuccessHandler() { + SimpleUrlLogoutSuccessHandler urlLogoutHandler = new SimpleUrlLogoutSuccessHandler(); + urlLogoutHandler.setDefaultTargetUrl(logoutSuccessUrl); + if(defaultLogoutSuccessHandlerMappings.isEmpty()) { + return urlLogoutHandler; + } + DelegatingLogoutSuccessHandler successHandler = new DelegatingLogoutSuccessHandler(defaultLogoutSuccessHandlerMappings); + successHandler.setDefaultLogoutSuccessHandler(urlLogoutHandler); + return successHandler; } @Override diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.groovy index a4c174bb07..b36a126102 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.groovy @@ -26,6 +26,9 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur import org.springframework.security.config.annotation.web.configurers.LogoutConfigurerTests.RememberMeNoLogoutHandler; import org.springframework.security.web.authentication.RememberMeServices import org.springframework.security.web.authentication.logout.LogoutFilter +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; +import org.springframework.security.web.util.matcher.RequestMatcher /** * @@ -33,6 +36,24 @@ import org.springframework.security.web.authentication.logout.LogoutFilter */ class LogoutConfigurerTests extends BaseSpringSpec { + def defaultLogoutSuccessHandlerForNullLogoutHandler() { + setup: + LogoutConfigurer config = new LogoutConfigurer(); + when: + config.defaultLogoutSuccessHandlerFor(null, Mock(RequestMatcher)) + then: + thrown(IllegalArgumentException) + } + + def defaultLogoutSuccessHandlerForNullMatcher() { + setup: + LogoutConfigurer config = new LogoutConfigurer(); + when: + config.defaultLogoutSuccessHandlerFor(Mock(LogoutSuccessHandler), null) + then: + thrown(IllegalArgumentException) + } + def "logout ObjectPostProcessor"() { setup: AnyObjectPostProcessor opp = Mock() @@ -145,4 +166,35 @@ class LogoutConfigurerTests extends BaseSpringSpec { .rememberMeServices(REMEMBER_ME) } } + + def "LogoutConfigurer content negotiation default redirects"() { + setup: + loadConfig(LogoutHandlerContentNegotiation) + when: + login() + request.method = 'POST' + request.servletPath = '/logout' + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == 302 + response.redirectedUrl == '/login?logout' + } + + // gh-3282 + def "LogoutConfigurer content negotiation json 201"() { + setup: + loadConfig(LogoutHandlerContentNegotiation) + when: + login() + request.method = 'POST' + request.servletPath = '/logout' + request.addHeader('Accept', 'application/json') + springSecurityFilterChain.doFilter(request,response,chain) + then: + response.status == 204 + } + + @EnableWebSecurity + static class LogoutHandlerContentNegotiation extends WebSecurityConfigurerAdapter { + } }