From 2d52fb8e4bffca409b2c7bd18a01a8eb5cfd7451 Mon Sep 17 00:00:00 2001 From: Marcus Da Coregio Date: Tue, 14 Mar 2023 09:26:46 -0300 Subject: [PATCH] Clear Repository on Logout --- .../web/configurers/LogoutConfigurer.java | 13 ++- .../configurers/LogoutConfigurerTests.java | 86 ++++++++++++++++++- .../pages/servlet/authentication/logout.adoc | 1 + .../logout/SecurityContextLogoutHandler.java | 16 ++++ .../HttpSessionSecurityContextRepository.java | 46 ++++++++-- .../SecurityContextLogoutHandlerTests.java | 42 ++++++++- ...SessionSecurityContextRepositoryTests.java | 50 ++++++++++- 7 files changed, 241 insertions(+), 13 deletions(-) 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 be65179302..130fb17a79 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,8 @@ import org.springframework.security.web.authentication.logout.LogoutSuccessHandl import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; @@ -325,6 +327,7 @@ public final class LogoutConfigurer> * @return the {@link LogoutFilter} to use. */ private LogoutFilter createLogoutFilter(H http) { + this.contextLogoutHandler.setSecurityContextRepository(getSecurityContextRepository(http)); this.logoutHandlers.add(this.contextLogoutHandler); this.logoutHandlers.add(postProcess(new LogoutSuccessEventPublishingLogoutHandler())); LogoutHandler[] handlers = this.logoutHandlers.toArray(new LogoutHandler[0]); @@ -334,6 +337,14 @@ public final class LogoutConfigurer> return result; } + private SecurityContextRepository getSecurityContextRepository(H http) { + SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class); + if (securityContextRepository == null) { + securityContextRepository = new HttpSessionSecurityContextRepository(); + } + return securityContextRepository; + } + private RequestMatcher getLogoutRequestMatcher(H http) { if (this.logoutRequestMatcher != null) { return this.logoutRequestMatcher; diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java index 1433e0ec1b..dedf10633a 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,9 @@ package org.springframework.security.config.annotation.web.configurers; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + import org.apache.http.HttpHeaders; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -23,7 +26,10 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -31,9 +37,12 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.web.SecurityFilterChain; 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.context.HttpSessionSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; @@ -42,6 +51,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; @@ -302,6 +312,80 @@ public class LogoutConfigurerTests { this.mvc.perform(post("/logout").with(csrf())).andExpect(status().isNotFound()); } + @Test + public void logoutWhenCustomSecurityContextRepositoryThenUses() throws Exception { + CustomSecurityContextRepositoryConfig.repository = mock(SecurityContextRepository.class); + this.spring.register(CustomSecurityContextRepositoryConfig.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder logoutRequest = post("/logout") + .with(csrf()) + .with(user("user")) + .header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML_VALUE); + this.mvc.perform(logoutRequest) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/login?logout")); + // @formatter:on + int invocationCount = 2; // 1 from user() post processor and 1 from + // SecurityContextLogoutHandler + verify(CustomSecurityContextRepositoryConfig.repository, times(invocationCount)).saveContext(any(), + any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + + @Test + public void logoutWhenNoSecurityContextRepositoryThenHttpSessionSecurityContextRepository() throws Exception { + this.spring.register(InvalidateHttpSessionFalseConfig.class).autowire(); + MockHttpSession session = mock(MockHttpSession.class); + // @formatter:off + MockHttpServletRequestBuilder logoutRequest = post("/logout") + .with(csrf()) + .with(user("user")) + .session(session) + .header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML_VALUE); + this.mvc.perform(logoutRequest) + .andExpect(status().isFound()) + .andExpect(redirectedUrl("/login?logout")) + .andReturn(); + // @formatter:on + verify(session).removeAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); + } + + @Configuration + @EnableWebSecurity + static class InvalidateHttpSessionFalseConfig { + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .logout((logout) -> logout.invalidateHttpSession(false)) + .securityContext((context) -> context.requireExplicitSave(true)); + return http.build(); + // @formatter:on + } + + } + + @Configuration + @EnableWebSecurity + static class CustomSecurityContextRepositoryConfig { + + static SecurityContextRepository repository; + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .logout(Customizer.withDefaults()) + .securityContext((context) -> context + .requireExplicitSave(true) + .securityContextRepository(repository) + ); + return http.build(); + // @formatter:on + } + + } + @EnableWebSecurity static class NullLogoutSuccessHandlerConfig extends WebSecurityConfigurerAdapter { diff --git a/docs/modules/ROOT/pages/servlet/authentication/logout.adoc b/docs/modules/ROOT/pages/servlet/authentication/logout.adoc index 23a55e1110..8a15a5335a 100644 --- a/docs/modules/ROOT/pages/servlet/authentication/logout.adoc +++ b/docs/modules/ROOT/pages/servlet/authentication/logout.adoc @@ -10,6 +10,7 @@ The default is that accessing the URL `/logout` will log the user out by: - Invalidating the HTTP Session - Cleaning up any RememberMe authentication that was configured - Clearing the `SecurityContextHolder` +- Clearing the `SecurityContextRepository` - Redirect to `/login?logout` Similar to configuring login capabilities, however, you also have various options to further customize your logout requirements: diff --git a/web/src/main/java/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.java b/web/src/main/java/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.java index 1e84ab64eb..6458902ed8 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.java +++ b/web/src/main/java/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.java @@ -27,6 +27,8 @@ import org.springframework.core.log.LogMessage; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; import org.springframework.util.Assert; /** @@ -50,6 +52,8 @@ public class SecurityContextLogoutHandler implements LogoutHandler { private boolean clearAuthentication = true; + private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository(); + /** * Requires the request to be passed in. * @param request from which to obtain a HTTP session (cannot be null) @@ -73,6 +77,8 @@ public class SecurityContextLogoutHandler implements LogoutHandler { if (this.clearAuthentication) { context.setAuthentication(null); } + SecurityContext emptyContext = SecurityContextHolder.createEmptyContext(); + this.securityContextRepository.saveContext(emptyContext, request, response); } public boolean isInvalidateHttpSession() { @@ -100,4 +106,14 @@ public class SecurityContextLogoutHandler implements LogoutHandler { this.clearAuthentication = clearAuthentication; } + /** + * Sets the {@link SecurityContextRepository} to use. Default is + * {@link HttpSessionSecurityContextRepository}. + * @param securityContextRepository the {@link SecurityContextRepository} to use. + */ + public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) { + Assert.notNull(securityContextRepository, "securityContextRepository cannot be null"); + this.securityContextRepository = securityContextRepository; + } + } diff --git a/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java b/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java index 5bfff5684e..d9b9758287 100644 --- a/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java +++ b/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -137,13 +137,46 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils.getNativeResponse(response, SaveContextOnUpdateOrErrorResponseWrapper.class); if (responseWrapper == null) { - boolean httpSessionExists = request.getSession(false) != null; - SecurityContext initialContext = SecurityContextHolder.createEmptyContext(); - responseWrapper = new SaveToSessionResponseWrapper(response, request, httpSessionExists, initialContext); + saveContextInHttpSession(context, request); + return; } responseWrapper.saveContext(context); } + private void saveContextInHttpSession(SecurityContext context, HttpServletRequest request) { + if (isTransient(context) || isTransient(context.getAuthentication())) { + return; + } + SecurityContext emptyContext = generateNewContext(); + if (emptyContext.equals(context)) { + HttpSession session = request.getSession(false); + removeContextFromSession(context, session); + } + else { + boolean createSession = this.allowSessionCreation; + HttpSession session = request.getSession(createSession); + setContextInSession(context, session); + } + } + + private void setContextInSession(SecurityContext context, HttpSession session) { + if (session != null) { + session.setAttribute(this.springSecurityContextKey, context); + if (this.logger.isDebugEnabled()) { + this.logger.debug(LogMessage.format("Stored %s to HttpSession [%s]", context, session)); + } + } + } + + private void removeContextFromSession(SecurityContext context, HttpSession session) { + if (session != null) { + session.removeAttribute(this.springSecurityContextKey); + if (this.logger.isDebugEnabled()) { + this.logger.debug(LogMessage.format("Removed %s from HttpSession [%s]", context, session)); + } + } + } + @Override public boolean containsContext(HttpServletRequest request) { HttpSession session = request.getSession(false); @@ -369,11 +402,8 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo // We may have a new session, so check also whether the context attribute // is set SEC-1561 if (contextChanged(context) || httpSession.getAttribute(springSecurityContextKey) == null) { - httpSession.setAttribute(springSecurityContextKey, context); + HttpSessionSecurityContextRepository.this.saveContextInHttpSession(context, this.request); this.isSaveContextInvoked = true; - if (this.logger.isDebugEnabled()) { - this.logger.debug(LogMessage.format("Stored %s to HttpSession [%s]", context, httpSession)); - } } } } diff --git a/web/src/test/java/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandlerTests.java b/web/src/test/java/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandlerTests.java index 2efb3eb96a..a0882d5fce 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandlerTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,12 +27,19 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * @author Rob Winch - * */ public class SecurityContextLogoutHandlerTests { @@ -76,4 +83,35 @@ public class SecurityContextLogoutHandlerTests { assertThat(beforeContext.getAuthentication()).isSameAs(beforeAuthentication); } + @Test + public void logoutWhenSecurityContextRepositoryThenSaveEmptyContext() { + SecurityContextRepository repository = mock(SecurityContextRepository.class); + this.handler.setSecurityContextRepository(repository); + this.handler.logout(this.request, this.response, SecurityContextHolder.getContext().getAuthentication()); + verify(repository).saveContext(eq(SecurityContextHolder.createEmptyContext()), any(), any()); + } + + @Test + public void logoutWhenClearAuthenticationFalseThenSaveEmptyContext() { + SecurityContextRepository repository = mock(SecurityContextRepository.class); + this.handler.setSecurityContextRepository(repository); + this.handler.setClearAuthentication(false); + this.handler.logout(this.request, this.response, SecurityContextHolder.getContext().getAuthentication()); + verify(repository).saveContext(eq(SecurityContextHolder.createEmptyContext()), any(), any()); + } + + @Test + public void constructorWhenDefaultSecurityContextRepositoryThenHttpSessionSecurityContextRepository() { + SecurityContextRepository securityContextRepository = (SecurityContextRepository) ReflectionTestUtils + .getField(this.handler, "securityContextRepository"); + assertThat(securityContextRepository).isInstanceOf(HttpSessionSecurityContextRepository.class); + } + + @Test + public void setSecurityContextRepositoryWhenNullThenException() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> this.handler.setSecurityContextRepository(null)) + .withMessage("securityContextRepository cannot be null"); + } + } diff --git a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java index 26cf33f13e..371f157455 100644 --- a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java +++ b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,6 +53,7 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextImpl; import org.springframework.security.core.context.TransientSecurityContext; +import org.springframework.security.core.userdetails.PasswordEncodedUser; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; @@ -763,6 +764,53 @@ public class HttpSessionSecurityContextRepositoryTests { assertThat(session).isNull(); } + @Test + public void saveContextWhenSecurityContextEmptyThenRemoveAttributeFromSession() { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + SecurityContext emptyContext = SecurityContextHolder.createEmptyContext(); + MockHttpSession session = (MockHttpSession) request.getSession(true); + session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, emptyContext); + repo.saveContext(emptyContext, request, response); + Object attributeAfterSave = session + .getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); + assertThat(attributeAfterSave).isNull(); + } + + @Test + public void saveContextWhenSecurityContextEmptyAndNoSessionThenDoesNotCreateSession() { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + SecurityContext emptyContext = SecurityContextHolder.createEmptyContext(); + repo.saveContext(emptyContext, request, response); + assertThat(request.getSession(false)).isNull(); + } + + @Test + public void saveContextWhenSecurityContextThenSaveInSession() { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + SecurityContext context = createSecurityContext(PasswordEncodedUser.user()); + repo.saveContext(context, request, response); + Object savedContext = request.getSession() + .getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); + assertThat(savedContext).isEqualTo(context); + } + + @Test + public void saveContextWhenTransientAuthenticationThenDoNotSave() { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(new SomeTransientAuthentication()); + repo.saveContext(context, request, response); + assertThat(request.getSession(false)).isNull(); + } + private SecurityContext createSecurityContext(UserDetails userDetails) { UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated(userDetails, userDetails.getPassword(), userDetails.getAuthorities());