diff --git a/spring-security-modules/spring-security-web-login-2/README.md b/spring-security-modules/spring-security-web-login-2/README.md new file mode 100644 index 0000000000..826301c0fe --- /dev/null +++ b/spring-security-modules/spring-security-web-login-2/README.md @@ -0,0 +1,9 @@ +## Spring Security Login - 2 + +This module contains articles about login/logout mechanisms with Spring Security. + +## Relevant articles: + +- [Manual Logout With Spring Security](https://www.baeldung.com/spring-security-manual-logout) +- [How to Disable Spring Security Logout Redirects](https://www.baeldung.com/spring-security-disable-logout-redirects) +- More articles: [[<-- prev]](/spring-security-modules/spring-security-web-login) diff --git a/spring-security-modules/spring-security-web-login-2/pom.xml b/spring-security-modules/spring-security-web-login-2/pom.xml new file mode 100644 index 0000000000..d5e5a4426e --- /dev/null +++ b/spring-security-modules/spring-security-web-login-2/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + spring-security-web-login-2 + 0.0.1-SNAPSHOT + spring-security-web-login-2 + jar + + + com.baeldung + parent-boot-2 + 0.0.1-SNAPSHOT + ../../parent-boot-2 + + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.thymeleaf.extras + thymeleaf-extras-springsecurity5 + + + org.springframework + spring-test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/spring-security-modules/spring-security-web-login-2/src/main/java/com/baeldung/logoutredirects/LogoutApplication.java b/spring-security-modules/spring-security-web-login-2/src/main/java/com/baeldung/logoutredirects/LogoutApplication.java new file mode 100644 index 0000000000..ef8175ffb2 --- /dev/null +++ b/spring-security-modules/spring-security-web-login-2/src/main/java/com/baeldung/logoutredirects/LogoutApplication.java @@ -0,0 +1,13 @@ +package com.baeldung.logoutredirects; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class LogoutApplication { + + public static void main(String[] args) { + SpringApplication.run(LogoutApplication.class, args); + } + +} diff --git a/spring-security-modules/spring-security-web-login-2/src/main/java/com/baeldung/logoutredirects/securityconfig/SpringSecurityConfig.java b/spring-security-modules/spring-security-web-login-2/src/main/java/com/baeldung/logoutredirects/securityconfig/SpringSecurityConfig.java new file mode 100644 index 0000000000..64141f63d8 --- /dev/null +++ b/spring-security-modules/spring-security-web-login-2/src/main/java/com/baeldung/logoutredirects/securityconfig/SpringSecurityConfig.java @@ -0,0 +1,27 @@ +package com.baeldung.logoutredirects.securityconfig; + +import javax.servlet.http.HttpServletResponse; + +import org.springframework.context.annotation.Configuration; +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 SpringSecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeRequests(authz -> authz.mvcMatchers("/login") + .permitAll() + .anyRequest() + .authenticated()) + .logout(logout -> logout.permitAll() + .logoutSuccessHandler((request, response, authentication) -> { + response.setStatus(HttpServletResponse.SC_OK); + })); + + } + +} diff --git a/spring-security-modules/spring-security-web-login-2/src/main/java/com/baeldung/manuallogout/ManualLogoutApplication.java b/spring-security-modules/spring-security-web-login-2/src/main/java/com/baeldung/manuallogout/ManualLogoutApplication.java new file mode 100644 index 0000000000..50ea356f03 --- /dev/null +++ b/spring-security-modules/spring-security-web-login-2/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-security-modules/spring-security-web-login-2/src/main/java/com/baeldung/manuallogout/SimpleSecurityConfiguration.java b/spring-security-modules/spring-security-web-login-2/src/main/java/com/baeldung/manuallogout/SimpleSecurityConfiguration.java new file mode 100644 index 0000000000..303a139215 --- /dev/null +++ b/spring-security-modules/spring-security-web-login-2/src/main/java/com/baeldung/manuallogout/SimpleSecurityConfiguration.java @@ -0,0 +1,95 @@ +package com.baeldung.manuallogout; + +import static org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter.Directive.CACHE; +import static org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter.Directive.COOKIES; +import static org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter.Directive.EXECUTION_CONTEXTS; +import static org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter.Directive.STORAGE; + +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +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.authentication.logout.HeaderWriterLogoutHandler; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; +import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter; + +@Configuration +@EnableWebSecurity +public class SimpleSecurityConfiguration { + + private static Logger logger = LoggerFactory.getLogger(SimpleSecurityConfiguration.class); + + @Order(4) + @Configuration + public static class LogoutOnRequestConfiguration extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http.antMatcher("/request/**") + .authorizeRequests(authz -> authz.anyRequest() + .permitAll()) + .logout(logout -> logout.logoutUrl("/request/logout") + .addLogoutHandler((request, response, auth) -> { + try { + request.logout(); + } catch (ServletException e) { + logger.error(e.getMessage()); + } + })); + } + } + + @Order(3) + @Configuration + public static class DefaultLogoutConfiguration extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http.antMatcher("/basic/**") + .authorizeRequests(authz -> authz.anyRequest() + .permitAll()) + .logout(logout -> logout.logoutUrl("/basic/basiclogout")); + } + } + + @Order(2) + @Configuration + public static class AllCookieClearingLogoutConfiguration extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http.antMatcher("/cookies/**") + .authorizeRequests(authz -> authz.anyRequest() + .permitAll()) + .logout(logout -> logout.logoutUrl("/cookies/cookielogout") + .addLogoutHandler(new SecurityContextLogoutHandler()) + .addLogoutHandler((request, response, auth) -> { + for (Cookie cookie : request.getCookies()) { + String cookieName = cookie.getName(); + Cookie cookieToDelete = new Cookie(cookieName, null); + cookieToDelete.setMaxAge(0); + response.addCookie(cookieToDelete); + } + })); + } + } + + @Order(1) + @Configuration + public static class ClearSiteDataHeaderLogoutConfiguration extends WebSecurityConfigurerAdapter { + + private static final ClearSiteDataHeaderWriter.Directive[] SOURCE = { CACHE, COOKIES, STORAGE, EXECUTION_CONTEXTS }; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.antMatcher("/csd/**") + .authorizeRequests(authz -> authz.anyRequest() + .permitAll()) + .logout(logout -> logout.logoutUrl("/csd/csdlogout") + .addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(SOURCE)))); + } + } +} diff --git a/spring-security-modules/spring-security-web-login-2/src/main/resources/application.properties b/spring-security-modules/spring-security-web-login-2/src/main/resources/application.properties new file mode 100644 index 0000000000..7fc55efed9 --- /dev/null +++ b/spring-security-modules/spring-security-web-login-2/src/main/resources/application.properties @@ -0,0 +1,5 @@ +server.port=8081 + +logging.level.root=INFO + +logging.level.org.springframework.security=DEBUG \ No newline at end of file diff --git a/spring-security-modules/spring-security-web-login-2/src/main/resources/logback.xml b/spring-security-modules/spring-security-web-login-2/src/main/resources/logback.xml new file mode 100644 index 0000000000..7d900d8ea8 --- /dev/null +++ b/spring-security-modules/spring-security-web-login-2/src/main/resources/logback.xml @@ -0,0 +1,13 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/spring-security-modules/spring-security-web-login-2/src/test/java/com/baeldung/logoutredirects/LogoutApplicationUnitTest.java b/spring-security-modules/spring-security-web-login-2/src/test/java/com/baeldung/logoutredirects/LogoutApplicationUnitTest.java new file mode 100644 index 0000000000..519a6bdc99 --- /dev/null +++ b/spring-security-modules/spring-security-web-login-2/src/test/java/com/baeldung/logoutredirects/LogoutApplicationUnitTest.java @@ -0,0 +1,34 @@ +package com.baeldung.logoutredirects; + +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.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +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.MockMvcResultMatchers.*; + +@RunWith(SpringRunner.class) +@WebMvcTest() +public class LogoutApplicationUnitTest { + + @Autowired + private MockMvc mockMvc; + + @WithMockUser(value = "spring") + @Test + public void whenLogout_thenDisableRedirect() throws Exception { + + this.mockMvc.perform(post("/logout").with(csrf())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").doesNotExist()) + .andExpect(unauthenticated()) + .andReturn(); + } + +} \ No newline at end of file diff --git a/spring-security-modules/spring-security-web-login-2/src/test/java/com/baeldung/manuallogout/ManualLogoutIntegrationTest.java b/spring-security-modules/spring-security-web-login-2/src/test/java/com/baeldung/manuallogout/ManualLogoutIntegrationTest.java new file mode 100644 index 0000000000..06dc01e116 --- /dev/null +++ b/spring-security-modules/spring-security-web-login-2/src/test/java/com/baeldung/manuallogout/ManualLogoutIntegrationTest.java @@ -0,0 +1,96 @@ +package com.baeldung.manuallogout; + +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.cookie; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpSession; + +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; + +@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 givenLoggedUserWhenUserLogoutThenSessionClearedAndNecessaryCookieCleared() throws Exception { + + this.mockMvc.perform(post("/basic/basiclogout").secure(true) + .with(csrf())) + .andExpect(status().is3xxRedirection()) + .andExpect(unauthenticated()) + .andReturn(); + } + + @WithMockUser(value = "spring") + @Test + public void givenLoggedUserWhenUserLogoutThenSessionClearedAndAllCookiesCleared() 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("/cookies/cookielogout").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("/csd/csdlogout").secure(true) + .with(csrf())) + .andDo(print()) + .andExpect(status().is3xxRedirection()) + .andExpect(header().exists(CLEAR_SITE_DATA_HEADER)) + .andReturn(); + } + + @WithMockUser(value = "spring") + @Test + public void givenLoggedUserWhenUserLogoutOnRequestThenSessionCleared() throws Exception { + + this.mockMvc.perform(post("/request/logout").secure(true) + .with(csrf())) + .andExpect(status().is3xxRedirection()) + .andExpect(unauthenticated()) + .andReturn(); + } +} \ No newline at end of file diff --git a/spring-security-modules/spring-security-web-login-2/src/test/resources/logback-test.xml b/spring-security-modules/spring-security-web-login-2/src/test/resources/logback-test.xml new file mode 100644 index 0000000000..8d4771e308 --- /dev/null +++ b/spring-security-modules/spring-security-web-login-2/src/test/resources/logback-test.xml @@ -0,0 +1,12 @@ + + + + + [%d{ISO8601}]-[%thread] %-5level %logger - %msg%n + + + + + + + \ No newline at end of file