From 6a7378ea0db47105280c294b61051595ccdc0f4a Mon Sep 17 00:00:00 2001 From: SippingCode <55111084+SippingCode@users.noreply.github.com> Date: Wed, 15 Apr 2020 07:26:52 +0200 Subject: [PATCH] BAEL 3970 - Manual logout with Spring Security (#9075) * Manual logout with Spring Security - Basic manual logout - logout with Clear Data Site Header * Add missing annotation for controller. Change mapping URL value. * Add intergration tests for manual logouts. * BAEL-3970 - Add asserts on test. Fix tests names. Remove unused imports. --- .../manuallogout/BasicAuthController.java | 32 +++++++++ .../manuallogout/ClearSiteDataController.java | 29 ++++++++ .../manuallogout/ManualLogoutApplication.java | 11 +++ .../SimpleSecurityConfiguration.java | 39 +++++++++++ .../ManualLogoutIntegrationTest.java | 70 +++++++++++++++++++ 5 files changed, 181 insertions(+) create mode 100644 spring-5-security/src/main/java/com/baeldung/manuallogout/BasicAuthController.java create mode 100644 spring-5-security/src/main/java/com/baeldung/manuallogout/ClearSiteDataController.java create mode 100644 spring-5-security/src/main/java/com/baeldung/manuallogout/ManualLogoutApplication.java create mode 100644 spring-5-security/src/main/java/com/baeldung/manuallogout/SimpleSecurityConfiguration.java create mode 100644 spring-5-security/src/test/java/com/baeldung/manuallogout/ManualLogoutIntegrationTest.java diff --git a/spring-5-security/src/main/java/com/baeldung/manuallogout/BasicAuthController.java b/spring-5-security/src/main/java/com/baeldung/manuallogout/BasicAuthController.java new file mode 100644 index 0000000000..8f01940dce --- /dev/null +++ b/spring-5-security/src/main/java/com/baeldung/manuallogout/BasicAuthController.java @@ -0,0 +1,32 @@ +package com.baeldung.manuallogout; + +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +@Controller +public class BasicAuthController { + + @RequestMapping(value = {"/basiclogout"}, method = RequestMethod.POST) + public String logout(HttpServletRequest request, HttpServletResponse response) { + HttpSession session; + SecurityContextHolder.clearContext(); + session = request.getSession(false); + if (session != null) { + session.invalidate(); + } + for (Cookie cookie : request.getCookies()) { + String cookieName = cookie.getName(); + Cookie cookieToDelete = new Cookie(cookieName, null); + cookieToDelete.setMaxAge(0); + response.addCookie(cookieToDelete); + } + return "redirect:/login?logout"; + } +} diff --git a/spring-5-security/src/main/java/com/baeldung/manuallogout/ClearSiteDataController.java b/spring-5-security/src/main/java/com/baeldung/manuallogout/ClearSiteDataController.java new file mode 100644 index 0000000000..7eef397da3 --- /dev/null +++ b/spring-5-security/src/main/java/com/baeldung/manuallogout/ClearSiteDataController.java @@ -0,0 +1,29 @@ +package com.baeldung.manuallogout; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.logout.HeaderWriterLogoutHandler; +import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter; +import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter.Directive; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@Controller +public class ClearSiteDataController { + + Directive[] SOURCE = {Directive.COOKIES, Directive.STORAGE, Directive.EXECUTION_CONTEXTS, Directive.CACHE}; + + @RequestMapping(value = {"/csdlogout"}, method = RequestMethod.POST) + public String logout(HttpServletRequest request, HttpServletResponse response) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null) { + ClearSiteDataHeaderWriter csdHeaderWriter = new ClearSiteDataHeaderWriter(SOURCE); + new HeaderWriterLogoutHandler(csdHeaderWriter).logout(request, response, auth); + } + return "redirect:/login?logout"; + } +} diff --git a/spring-5-security/src/main/java/com/baeldung/manuallogout/ManualLogoutApplication.java b/spring-5-security/src/main/java/com/baeldung/manuallogout/ManualLogoutApplication.java new file mode 100644 index 0000000000..50ea356f03 --- /dev/null +++ b/spring-5-security/src/main/java/com/baeldung/manuallogout/ManualLogoutApplication.java @@ -0,0 +1,11 @@ +package com.baeldung.manuallogout; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ManualLogoutApplication { + public static void main(String[] args) { + SpringApplication.run(ManualLogoutApplication.class, args); + } +} diff --git a/spring-5-security/src/main/java/com/baeldung/manuallogout/SimpleSecurityConfiguration.java b/spring-5-security/src/main/java/com/baeldung/manuallogout/SimpleSecurityConfiguration.java new file mode 100644 index 0000000000..6f14f6fca2 --- /dev/null +++ b/spring-5-security/src/main/java/com/baeldung/manuallogout/SimpleSecurityConfiguration.java @@ -0,0 +1,39 @@ +package com.baeldung.manuallogout; + +import org.springframework.context.annotation.Configuration; +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; + +@Configuration +@EnableWebSecurity +public class SimpleSecurityConfiguration extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.formLogin() + .loginProcessingUrl("/login") + .loginPage("/login") + .usernameParameter("username") + .passwordParameter("password") + .defaultSuccessUrl("/") + .failureUrl("/login?error"); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.inMemoryAuthentication() + .withUser("user") + .password("password") + .roles("USER") + .and() + .withUser("manager") + .password("password") + .credentialsExpired(true) + .accountExpired(true) + .accountLocked(true) + .authorities("WRITE_PRIVILEGES", "READ_PRIVILEGES") + .roles("MANAGER"); + } +} diff --git a/spring-5-security/src/test/java/com/baeldung/manuallogout/ManualLogoutIntegrationTest.java b/spring-5-security/src/test/java/com/baeldung/manuallogout/ManualLogoutIntegrationTest.java new file mode 100644 index 0000000000..a64cb82910 --- /dev/null +++ b/spring-5-security/src/test/java/com/baeldung/manuallogout/ManualLogoutIntegrationTest.java @@ -0,0 +1,70 @@ +package com.baeldung.manuallogout; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpSession; + +import static org.junit.Assert.assertNull; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@RunWith(SpringRunner.class) +@WebMvcTest() +public class ManualLogoutIntegrationTest { + + private static final String CLEAR_SITE_DATA_HEADER = "Clear-Site-Data"; + public static final int EXPIRY = 60 * 10; + public static final String COOKIE_NAME = "customerName"; + public static final String COOKIE_VALUE = "myName"; + public static final String ATTRIBUTE_NAME = "att"; + public static final String ATTRIBUTE_VALUE = "attvalue"; + + @Autowired + private MockMvc mockMvc; + + @WithMockUser(value = "spring") + @Test + public void givenLoggedUserWhenUserLogoutThenSessionCleared() throws Exception { + + MockHttpSession session = new MockHttpSession(); + session.setAttribute(ATTRIBUTE_NAME, ATTRIBUTE_VALUE); + + Cookie randomCookie = new Cookie(COOKIE_NAME, COOKIE_VALUE); + randomCookie.setMaxAge(EXPIRY); // 10 minutes + + MockHttpServletRequest requestStateAfterLogout = this.mockMvc.perform(post("/basiclogout").secure(true).with(csrf()).session(session).cookie(randomCookie)) + .andExpect(status().is3xxRedirection()) + .andExpect(unauthenticated()) + .andExpect(cookie().maxAge(COOKIE_NAME, 0)) + .andReturn() + .getRequest(); + + HttpSession sessionStateAfterLogout = requestStateAfterLogout.getSession(); + assertNull(sessionStateAfterLogout.getAttribute(ATTRIBUTE_NAME)); + + + } + + @WithMockUser(value = "spring") + @Test + public void givenLoggedUserWhenUserLogoutThenClearDataSiteHeaderPresent() throws Exception { + + this.mockMvc.perform(post("/csdlogout").secure(true).with(csrf())) + .andDo(print()) + .andExpect(status().is3xxRedirection()) + .andExpect(header().exists(CLEAR_SITE_DATA_HEADER)) + .andReturn(); + } +} \ No newline at end of file