diff --git a/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy deleted file mode 100644 index 15cf40a831..0000000000 --- a/config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy +++ /dev/null @@ -1,430 +0,0 @@ -/* - * Copyright 2002-2013 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. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.security.config.http - -import org.springframework.mock.web.MockFilterChain -import org.springframework.mock.web.MockHttpServletRequest -import org.springframework.mock.web.MockHttpServletResponse -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken -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.core.session.SessionRegistry -import org.springframework.security.core.session.SessionRegistryImpl -import org.springframework.security.core.userdetails.User -import org.springframework.security.util.FieldUtils -import org.springframework.security.web.FilterChainProxy -import org.springframework.security.web.authentication.RememberMeServices -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter -import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler -import org.springframework.security.web.authentication.logout.LogoutFilter -import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler -import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter -import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy -import org.springframework.security.web.context.NullSecurityContextRepository -import org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper -import org.springframework.security.web.context.SecurityContextPersistenceFilter -import org.springframework.security.web.savedrequest.RequestCacheAwareFilter -import org.springframework.security.web.session.ConcurrentSessionFilter -import org.springframework.security.web.session.SessionManagementFilter - -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse - -import static org.junit.Assert.assertSame -import static org.mockito.Matchers.any -import static org.mockito.Mockito.verify - -/** - * Tests session-related functionality for the <http> namespace element and <session-management> - * - * @author Luke Taylor - * @author Rob Winch - */ -class SessionManagementConfigTests extends AbstractHttpConfigTests { - - def settingCreateSessionToAlwaysSetsFilterPropertiesCorrectly() { - httpCreateSession('always') { } - createAppContext(); - - def filter = getFilter(SecurityContextPersistenceFilter.class); - - expect: - filter.forceEagerSessionCreation - filter.repo.allowSessionCreation - filter.repo.disableUrlRewriting - } - - def settingCreateSessionToNeverSetsFilterPropertiesCorrectly() { - httpCreateSession('never') { } - createAppContext(); - - def filter = getFilter(SecurityContextPersistenceFilter.class); - - expect: - !filter.forceEagerSessionCreation - !filter.repo.allowSessionCreation - } - - def settingCreateSessionToStatelessSetsFilterPropertiesCorrectly() { - httpCreateSession('stateless') { } - createAppContext(); - - def filter = getFilter(SecurityContextPersistenceFilter.class); - - expect: - !filter.forceEagerSessionCreation - filter.repo instanceof NullSecurityContextRepository - getFilter(SessionManagementFilter.class) == null - getFilter(RequestCacheAwareFilter.class) == null - } - - def settingCreateSessionToIfRequiredDoesntCreateASessionForPublicInvocation() { - httpCreateSession('ifRequired') { } - createAppContext(); - - def filter = getFilter(SecurityContextPersistenceFilter.class); - - expect: - !filter.forceEagerSessionCreation - filter.repo.allowSessionCreation - } - - def 'SEC-1208: Session is not created when rejecting user due to max sessions exceeded'() { - setup: - httpCreateSession('never') { - 'session-management'() { - 'concurrency-control'('max-sessions':1,'error-if-maximum-exceeded':'true') - } - csrf(disabled:true) - } - createAppContext() - SessionRegistry registry = appContext.getBean(SessionRegistry) - registry.registerNewSession("1", new User("user","password",AuthorityUtils.createAuthorityList("ROLE_USER"))) - MockHttpServletRequest request = new MockHttpServletRequest("GET", "") - MockHttpServletResponse response = new MockHttpServletResponse() - String credentials = "user:password" - request.addHeader("Authorization", "Basic " + credentials.bytes.encodeBase64()) - when: "exceed max authentication attempts" - appContext.getBean(FilterChainProxy).doFilter(request, response, new MockFilterChain()) - then: "no new session is created" - request.getSession(false) == null - response.status == HttpServletResponse.SC_UNAUTHORIZED - } - - def 'SEC-2137: disable session fixation and enable concurrency control'() { - setup: "context where session fixation is disabled and concurrency control is enabled" - httpAutoConfig { - 'session-management'('session-fixation-protection':'none') { - 'concurrency-control'('max-sessions':'1','error-if-maximum-exceeded':'true') - } - } - createAppContext() - MockHttpServletRequest request = new MockHttpServletRequest("GET", "") - MockHttpServletResponse response = new MockHttpServletResponse() - String originalSessionId = request.session.id - String credentials = "user:password" - request.addHeader("Authorization", "Basic " + credentials.bytes.encodeBase64()) - when: "authenticate" - appContext.getBean(FilterChainProxy).doFilter(request, response, new MockFilterChain()) - then: "session invalidate is not called" - request.session.id == originalSessionId - } - - def httpCreateSession(String create, Closure c) { - xml.http(['auto-config': 'true', 'create-session': create], c) - } - - def concurrentSessionSupportAddsFilterAndExpectedBeans() { - when: - httpAutoConfig { - 'session-management'() { - 'concurrency-control'('session-registry-alias':'sr', 'expired-url': '/expired') - } - csrf(disabled:true) - } - createAppContext(); - List filters = getFilters("/someurl"); - def concurrentSessionFilter = filters.get(1) - - then: - concurrentSessionFilter instanceof ConcurrentSessionFilter - concurrentSessionFilter.sessionInformationExpiredStrategy.destinationUrl == '/expired' - appContext.getBean("sr") != null - getFilter(SessionManagementFilter.class) != null - sessionRegistryIsValid(); - - concurrentSessionFilter.handlers.logoutHandlers.size() == 1 - def logoutHandler = concurrentSessionFilter.handlers.logoutHandlers[0] - logoutHandler instanceof SecurityContextLogoutHandler - logoutHandler.invalidateHttpSession - - } - - def 'concurrency-control adds custom logout handlers'() { - when: 'Custom logout and remember-me' - httpAutoConfig { - 'session-management'() { - 'concurrency-control'() - } - 'logout'('invalidate-session': false, 'delete-cookies': 'testCookie') - 'remember-me'() - csrf(disabled:true) - } - createAppContext() - - List filters = getFilters("/someurl") - ConcurrentSessionFilter concurrentSessionFilter = filters.get(1) - def logoutHandlers = concurrentSessionFilter.handlers.logoutHandlers - - then: 'ConcurrentSessionFilter contains the customized LogoutHandlers' - logoutHandlers.size() == 3 - def securityCtxlogoutHandler = logoutHandlers.find { it instanceof SecurityContextLogoutHandler } - securityCtxlogoutHandler.invalidateHttpSession == false - def cookieClearingLogoutHandler = logoutHandlers.find { it instanceof CookieClearingLogoutHandler } - cookieClearingLogoutHandler.cookiesToClear == ['testCookie'] - def remembermeLogoutHandler = logoutHandlers.find { it instanceof RememberMeServices } - remembermeLogoutHandler == getFilter(RememberMeAuthenticationFilter.class).rememberMeServices - } - - def 'concurrency-control with remember-me and no LogoutFilter contains SecurityContextLogoutHandler and RememberMeServices as LogoutHandlers'() { - when: 'RememberMe and No LogoutFilter' - xml.http(['entry-point-ref': 'entryPoint'], { - 'session-management'() { - 'concurrency-control'() - } - 'remember-me'() - csrf(disabled:true) - }) - bean('entryPoint', 'org.springframework.security.web.authentication.Http403ForbiddenEntryPoint') - createAppContext() - - List filters = getFilters("/someurl") - ConcurrentSessionFilter concurrentSessionFilter = filters.get(1) - def logoutHandlers = concurrentSessionFilter.handlers.logoutHandlers - - then: 'SecurityContextLogoutHandler and RememberMeServices are in ConcurrentSessionFilter logoutHandlers' - !filters.find { it instanceof LogoutFilter } - logoutHandlers.size() == 2 - def securityCtxlogoutHandler = logoutHandlers.find { it instanceof SecurityContextLogoutHandler } - securityCtxlogoutHandler.invalidateHttpSession == true - logoutHandlers.find { it instanceof RememberMeServices } == getFilter(RememberMeAuthenticationFilter).rememberMeServices - } - - def 'concurrency-control with no remember-me or LogoutFilter contains SecurityContextLogoutHandler as LogoutHandlers'() { - when: 'No Logout Filter or RememberMe' - xml.http(['entry-point-ref': 'entryPoint'], { - 'session-management'() { - 'concurrency-control'() - } - }) - bean('entryPoint', 'org.springframework.security.web.authentication.Http403ForbiddenEntryPoint') - createAppContext() - - List filters = getFilters("/someurl") - ConcurrentSessionFilter concurrentSessionFilter = filters.get(1) - def logoutHandlers = concurrentSessionFilter.handlers.logoutHandlers - - then: 'Only SecurityContextLogoutHandler is found in ConcurrentSessionFilter logoutHandlers' - !filters.find { it instanceof LogoutFilter } - logoutHandlers.size() == 1 - def securityCtxlogoutHandler = logoutHandlers.find { it instanceof SecurityContextLogoutHandler } - securityCtxlogoutHandler.invalidateHttpSession == true - } - - def 'SEC-2057: ConcurrentSessionFilter is after SecurityContextPersistenceFilter'() { - httpAutoConfig { - 'session-management'() { - 'concurrency-control'() - } - } - createAppContext() - List filters = getFilters("/someurl") - - expect: - filters.get(0) instanceof SecurityContextPersistenceFilter - filters.get(1) instanceof ConcurrentSessionFilter - } - - def 'concurrency-control handles default expired-url as null'() { - httpAutoConfig { - 'session-management'() { - 'concurrency-control'('session-registry-alias':'sr') - } - } - createAppContext(); - List filters = getFilters("/someurl"); - - expect: - filters.get(1).sessionInformationExpiredStrategy.class.name == 'org.springframework.security.web.session.ConcurrentSessionFilter$ResponseBodySessionInformationExpiredStrategy' - } - - def externalSessionStrategyIsSupported() { - setup: - httpAutoConfig { - 'session-management'('session-authentication-strategy-ref':'ss') - csrf(disabled:true) - } - mockBean(SessionAuthenticationStrategy,'ss') - createAppContext() - - MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); - request.getSession(); - request.servletPath = "/login" - request.setMethod("POST"); - request.setParameter("username", "user"); - request.setParameter("password", "password"); - - SessionAuthenticationStrategy sessionAuthStrategy = appContext.getBean('ss',SessionAuthenticationStrategy) - FilterChainProxy springSecurityFilterChain = appContext.getBean(FilterChainProxy) - when: - springSecurityFilterChain.doFilter(request,new MockHttpServletResponse(), new MockFilterChain()) - then: "CustomSessionAuthenticationStrategy has seen the request (although REQUEST is a wrapped request)" - verify(sessionAuthStrategy).onAuthentication(any(Authentication), any(HttpServletRequest), any(HttpServletResponse)) - } - - def externalSessionRegistryBeanIsConfiguredCorrectly() { - httpAutoConfig { - 'session-management'() { - 'concurrency-control'('session-registry-ref':'sr') - } - csrf(disabled:true) - } - bean('sr', SessionRegistryImpl.class.name) - createAppContext(); - - expect: - sessionRegistryIsValid(); - } - - def sessionRegistryIsValid() { - Object sessionRegistry = appContext.getBean("sr"); - Object sessionRegistryFromConcurrencyFilter = FieldUtils.getFieldValue( - getFilter(ConcurrentSessionFilter.class), "sessionRegistry"); - Object sessionRegistryFromFormLoginFilter = FieldUtils.getFieldValue(getFilter(UsernamePasswordAuthenticationFilter),"sessionStrategy").delegateStrategies[0].sessionRegistry - Object sessionRegistryFromMgmtFilter = FieldUtils.getFieldValue(getFilter(SessionManagementFilter),"sessionAuthenticationStrategy").delegateStrategies[0].sessionRegistry - - assertSame(sessionRegistry, sessionRegistryFromConcurrencyFilter); - assertSame(sessionRegistry, sessionRegistryFromMgmtFilter); - // SEC-1143 - assertSame(sessionRegistry, sessionRegistryFromFormLoginFilter); - true; - } - - def concurrentSessionMaxSessionsIsCorrectlyConfigured() { - setup: - httpAutoConfig { - 'session-management'('session-authentication-error-url':'/max-exceeded') { - 'concurrency-control'('max-sessions': '2', 'error-if-maximum-exceeded':'true') - } - } - createAppContext(); - - def seshFilter = getFilter(SessionManagementFilter.class); - def auth = new UsernamePasswordAuthenticationToken("bob", "pass"); - SecurityContextHolder.getContext().setAuthentication(auth); - MockHttpServletResponse mockResponse = new MockHttpServletResponse(); - def response = new SaveContextOnUpdateOrErrorResponseWrapper(mockResponse, false) { - protected void saveContext(SecurityContext context) { - } - }; - when: "First session is established" - seshFilter.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()); - then: "ok" - mockResponse.redirectedUrl == null - when: "Second session is established" - seshFilter.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()); - then: "ok" - mockResponse.redirectedUrl == null - when: "Third session is established" - seshFilter.doFilter(new MockHttpServletRequest("GET", ""), response, new MockFilterChain()); - then: "Rejected" - mockResponse.redirectedUrl == "/max-exceeded"; - } - - def disablingSessionProtectionRemovesSessionManagementFilterIfNoInvalidSessionUrlSet() { - httpAutoConfig { - 'session-management'('session-fixation-protection': 'none') - csrf(disabled:true) - } - createAppContext() - - expect: - !(getFilters("/someurl").find { it instanceof SessionManagementFilter}) - } - - def 'session-fixation-protection=none'() { - setup: - MockHttpServletRequest request = new MockHttpServletRequest(method:'POST') - request.session.id = '123' - request.setParameter('username', 'user') - request.setParameter('password', 'password') - request.servletPath = '/login' - - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - httpAutoConfig { - 'session-management'('session-fixation-protection': 'none') - csrf(disabled:true) - } - createAppContext() - request.session.id = '123' - - when: - springSecurityFilterChain.doFilter(request,response, chain) - - then: - request.session.id == '123' - } - - def 'session-fixation-protection=migrateSession'() { - setup: - MockHttpServletRequest request = new MockHttpServletRequest(method:'POST') - request.setParameter('username', 'user') - request.setParameter('password', 'password') - request.servletPath = '/login' - - MockHttpServletResponse response = new MockHttpServletResponse() - MockFilterChain chain = new MockFilterChain() - httpAutoConfig { - 'session-management'('session-fixation-protection': 'migrateSession') - csrf(disabled:true) - } - createAppContext() - String originalId = request.session.id - - when: - springSecurityFilterChain.doFilter(request,response, chain) - - then: - request.session.id != originalId - } - - def disablingSessionProtectionRetainsSessionManagementFilterInvalidSessionUrlSet() { - httpAutoConfig { - 'session-management'('session-fixation-protection': 'none', 'invalid-session-url': '/timeoutUrl') - csrf(disabled:true) - } - createAppContext() - def filter = getFilters("/someurl")[11] - - expect: - filter instanceof SessionManagementFilter - filter.invalidSessionStrategy.destinationUrl == '/timeoutUrl' - } - -} diff --git a/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTests.java b/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTests.java new file mode 100644 index 0000000000..0ff6d2edf1 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTests.java @@ -0,0 +1,667 @@ +/* + * Copyright 2002-2018 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.http; + +import java.io.IOException; +import java.security.Principal; +import java.util.List; +import javax.servlet.Filter; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +import org.apache.http.HttpStatus; +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.session.SessionRegistry; +import org.springframework.security.util.FieldUtils; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.security.web.authentication.RememberMeServices; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.security.web.authentication.session.SessionAuthenticationException; +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; +import org.springframework.security.web.session.ConcurrentSessionFilter; +import org.springframework.security.web.session.SessionManagementFilter; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +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.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests session-related functionality for the <http> namespace element and <session-management> + * + * @author Luke Taylor + * @author Rob Winch + * @author Josh Cummings + */ +public class SessionManagementConfigTests { + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/SessionManagementConfigTests"; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + MockMvc mvc; + + @Test + public void requestWhenCreateSessionAlwaysThenAlwaysCreatesSession() + throws Exception { + this.spring.configLocations(this.xml("CreateSessionAlways")).autowire(); + + MockHttpServletRequest request = get("/").buildRequest(this.servletContext()); + MockHttpServletResponse response = request(request, this.spring.getContext()); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK); + assertThat(request.getSession(false)).isNotNull(); + } + + @Test + public void requestWhenCreateSessionIsSetToNeverThenDoesNotCreateSessionOnLoginChallenge() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionNever")).autowire(); + + MockHttpServletRequest request = get("/auth").buildRequest(this.servletContext()); + MockHttpServletResponse response = request(request, this.spring.getContext()); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY); + assertThat(request.getSession(false)).isNull(); + } + + @Test + public void requestWhenCreateSessionIsSetToNeverThenDoesNotCreateSessionOnLogin() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionNever")).autowire(); + + MockHttpServletRequest request = post("/login") + .param("username", "user") + .param("password", "password") + .buildRequest(this.servletContext()); + request = csrf().postProcessRequest(request); + MockHttpServletResponse response = request(request, this.spring.getContext()); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY); + assertThat(request.getSession(false)).isNull(); + } + + @Test + public void requestWhenCreateSessionIsSetToNeverThenUsesExistingSession() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionNever")).autowire(); + + MockHttpServletRequest request = post("/login") + .param("username", "user") + .param("password", "password") + .buildRequest(this.servletContext()); + request = csrf().postProcessRequest(request); + MockHttpSession session = new MockHttpSession(); + request.setSession(session); + MockHttpServletResponse response = request(request, this.spring.getContext()); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY); + assertThat(request.getSession(false)).isNotNull(); + assertThat(request.getSession(false).getAttribute(SPRING_SECURITY_CONTEXT_KEY)) + .isNotNull(); + } + + @Test + public void requestWhenCreateSessionIsSetToStatelessThenDoesNotCreateSessionOnLoginChallenge() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionStateless")).autowire(); + + this.mvc.perform(get("/auth")) + .andExpect(status().isFound()) + .andExpect(session().exists(false)); + } + + @Test + public void requestWhenCreateSessionIsSetToStatelessThenDoesNotCreateSessionOnLogin() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionStateless")).autowire(); + + + this.mvc.perform(post("/login") + .param("username", "user") + .param("password", "password") + .with(csrf())) + .andExpect(status().isFound()) + .andExpect(session().exists(false)); + } + + @Test + public void requestWhenCreateSessionIsSetToStatelessThenIgnoresExistingSession() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionStateless")).autowire(); + + MvcResult result = + this.mvc.perform(post("/login") + .param("username", "user") + .param("password", "password") + .session(new MockHttpSession()) + .with(csrf())) + .andExpect(status().isFound()) + .andExpect(session()) + .andReturn(); + + assertThat(result.getRequest().getSession(false).getAttribute(SPRING_SECURITY_CONTEXT_KEY)) + .isNull(); + } + + @Test + public void requestWhenCreateSessionIsSetToIfRequiredThenDoesNotCreateSessionOnPublicInvocation() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionIfRequired")).autowire(); + + ServletContext servletContext = this.mvc.getDispatcherServlet().getServletContext(); + MockHttpServletRequest request = get("/").buildRequest(servletContext); + MockHttpServletResponse response = request(request, this.spring.getContext()); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK); + assertThat(request.getSession(false)).isNull(); + } + + @Test + public void requestWhenCreateSessionIsSetToIfRequiredThenCreatesSessionOnLoginChallenge() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionIfRequired")).autowire(); + + ServletContext servletContext = this.mvc.getDispatcherServlet().getServletContext(); + MockHttpServletRequest request = get("/auth").buildRequest(servletContext); + MockHttpServletResponse response = request(request, this.spring.getContext()); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY); + assertThat(request.getSession(false)).isNotNull(); + } + + @Test + public void requestWhenCreateSessionIsSetToIfRequiredThenCreatesSessionOnLogin() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionIfRequired")).autowire(); + + ServletContext servletContext = this.mvc.getDispatcherServlet().getServletContext(); + MockHttpServletRequest request = post("/login") + .param("username", "user") + .param("password", "password") + .buildRequest(servletContext); + request = csrf().postProcessRequest(request); + MockHttpServletResponse response = request(request, this.spring.getContext()); + + assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_MOVED_TEMPORARILY); + assertThat(request.getSession(false)).isNotNull(); + } + + /** + * SEC-1208 + */ + @Test + public void requestWhenRejectingUserBasedOnMaxSessionsExceededThenDoesNotCreateSession() + throws Exception { + + this.spring.configLocations(this.xml("Sec1208")).autowire(); + + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()) + .andExpect(session()); + + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(status().isUnauthorized()) + .andExpect(session().exists(false)); + } + + /** + * SEC-2137 + */ + @Test + public void requestWhenSessionFixationProtectionDisabledAndConcurrencyControlEnabledThenSessionNotInvalidated() + throws Exception { + + this.spring.configLocations(this.xml("Sec2137")).autowire(); + + MockHttpSession session = new MockHttpSession(); + this.mvc.perform(get("/auth") + .session(session) + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()) + .andExpect(session().id(session.getId())); + } + + @Test + public void autowireWhenExportingSessionRegistryBeanThenAvailableForWiring() { + this.spring.configLocations(this.xml("ConcurrencyControlSessionRegistryAlias")).autowire(); + + this.sessionRegistryIsValid(); + } + + @Test + public void requestWhenExpiredUrlIsSetThenInvalidatesSessionAndRedirects() + throws Exception { + + this.spring.configLocations(this.xml("ConcurrencyControlExpiredUrl")).autowire(); + + this.mvc.perform(get("/auth") + .session(this.expiredSession()) + .with(httpBasic("user", "password"))) + .andExpect(redirectedUrl("/expired")) + .andExpect(session().exists(false)); + } + + @Test + public void requestWhenConcurrencyControlAndCustomLogoutHandlersAreSetThenAllAreInvokedWhenSessionExpires() + throws Exception { + + this.spring.configLocations(this.xml("ConcurrencyControlLogoutAndRememberMeHandlers")).autowire(); + + this.mvc.perform(get("/auth") + .session(this.expiredSession()) + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()) + .andExpect(cookie().maxAge("testCookie", 0)) + .andExpect(cookie().exists("rememberMeCookie")) + .andExpect(session().valid(true)); + } + + @Test + public void requestWhenConcurrencyControlAndRememberMeAreSetThenInvokedWhenSessionExpires() + throws Exception { + + this.spring.configLocations(this.xml("ConcurrencyControlRememberMeHandler")).autowire(); + + this.mvc.perform(get("/auth") + .session(this.expiredSession()) + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()) + .andExpect(cookie().exists("rememberMeCookie")) + .andExpect(session().exists(false)); + } + + /** + * SEC-2057 + */ + @Test + public void autowireWhenConcurrencyControlIsSetThenLogoutHandlersGetAuthenticationObject() + throws Exception { + + this.spring.configLocations(this.xml("ConcurrencyControlCustomLogoutHandler")).autowire(); + + MvcResult result = + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(session()) + .andReturn(); + + MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); + + SessionRegistry sessionRegistry = this.spring.getContext().getBean(SessionRegistry.class); + sessionRegistry.getSessionInformation(session.getId()).expireNow(); + + this.mvc.perform(get("/auth") + .session(session)) + .andExpect(header().string("X-Username", "user")); + } + + @Test + public void requestWhenConcurrencyControlIsSetThenDefaultsToResponseBodyExpirationResponse() + throws Exception { + + this.spring.configLocations(this.xml("ConcurrencyControlSessionRegistryAlias")).autowire(); + + this.mvc.perform(get("/auth") + .session(this.expiredSession()) + .with(httpBasic("user", "password"))) + .andExpect(content().string("This session has been expired (possibly due to multiple concurrent " + + "logins being attempted as the same user).")); + } + + @Test + public void requestWhenCustomSessionAuthenticationStrategyThenInvokesOnAuthentication() + throws Exception { + + this.spring.configLocations(this.xml("SessionAuthenticationStrategyRef")).autowire(); + + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(status().isIAmATeapot()); + } + + @Test + public void autowireWhenSessionRegistryRefIsSetThenAvailableForWiring() { + this.spring.configLocations(this.xml("ConcurrencyControlSessionRegistryRef")).autowire(); + + this.sessionRegistryIsValid(); + } + + @Test + public void requestWhenMaxSessionsIsSetThenErrorsWhenExceeded() + throws Exception { + + this.spring.configLocations(this.xml("ConcurrencyControlMaxSessions")).autowire(); + + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(redirectedUrl("/max-exceeded")); + } + + @Test + public void autowireWhenSessionFixationProtectionIsNoneAndCsrfDisabledThenSessionManagementFilterIsNotWired() { + + this.spring.configLocations(this.xml("NoSessionManagementFilter")).autowire(); + + assertThat(this.getFilter(SessionManagementFilter.class)).isNull(); + } + + @Test + public void requestWhenSessionFixationProtectionIsNoneThenSessionNotInvalidated() + throws Exception { + + this.spring.configLocations(this.xml("SessionFixationProtectionNone")).autowire(); + + MockHttpSession session = new MockHttpSession(); + String sessionId = session.getId(); + + this.mvc.perform(get("/auth") + .session(session) + .with(httpBasic("user", "password"))) + .andExpect(session().id(sessionId)); + } + + @Test + public void requestWhenSessionFixationProtectionIsMigrateSessionThenSessionIsReplaced() + throws Exception { + + this.spring.configLocations(this.xml("SessionFixationProtectionMigrateSession")).autowire(); + + MockHttpSession session = new MockHttpSession(); + String sessionId = session.getId(); + + MvcResult result = + this.mvc.perform(get("/auth") + .session(session) + .with(httpBasic("user", "password"))) + .andExpect(session()) + .andReturn(); + + assertThat(result.getRequest().getSession(false).getId()).isNotEqualTo(sessionId); + } + + @Test + public void requestWhenSessionFixationProtectionIsNoneAndInvalidSessionUrlIsSetThenStillRedirectsOnInvalidSession() + throws Exception { + + this.spring.configLocations(this.xml("SessionFixationProtectionNoneWithInvalidSessionUrl")).autowire(); + + this.mvc.perform(get("/auth") + .with(request -> { + request.setRequestedSessionId("1"); + request.setRequestedSessionIdValid(false); + return request; + })) + .andExpect(redirectedUrl("/timeoutUrl")); + } + + static class TeapotSessionAuthenticationStrategy implements SessionAuthenticationStrategy { + + @Override + public void onAuthentication( + Authentication authentication, + HttpServletRequest request, + HttpServletResponse response) throws SessionAuthenticationException { + + response.setStatus(org.springframework.http.HttpStatus.I_AM_A_TEAPOT.value()); + } + } + + static class CustomRememberMeServices implements RememberMeServices, LogoutHandler { + @Override + public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) { + return null; + } + + @Override + public void loginFail(HttpServletRequest request, HttpServletResponse response) { + + } + + @Override + public void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { + + } + + @Override + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + response.addHeader("X-Username", authentication.getName()); + } + } + + @RestController + static class BasicController { + @GetMapping("/") + public String ok() { + return "ok"; + } + + @GetMapping("/auth") + public String auth(Principal principal) { + return principal.getName(); + } + } + + private void sessionRegistryIsValid() { + SessionRegistry sessionRegistry = this.spring.getContext() + .getBean("sessionRegistry", SessionRegistry.class); + + assertThat(sessionRegistry).isNotNull(); + + assertThat(this.getFilter(ConcurrentSessionFilter.class)) + .returns(sessionRegistry, this::extractSessionRegistry); + assertThat(this.getFilter(UsernamePasswordAuthenticationFilter.class)) + .returns(sessionRegistry, this::extractSessionRegistry); + // SEC-1143 + assertThat(this.getFilter(SessionManagementFilter.class)) + .returns(sessionRegistry, this::extractSessionRegistry); + } + + private SessionRegistry extractSessionRegistry(ConcurrentSessionFilter filter) { + return getFieldValue(filter, "sessionRegistry"); + } + + private SessionRegistry extractSessionRegistry(UsernamePasswordAuthenticationFilter filter) { + SessionAuthenticationStrategy strategy = getFieldValue(filter, "sessionStrategy"); + List strategies = getFieldValue(strategy, "delegateStrategies"); + return getFieldValue(strategies.get(0), "sessionRegistry"); + } + + private SessionRegistry extractSessionRegistry(SessionManagementFilter filter) { + SessionAuthenticationStrategy strategy = getFieldValue(filter, "sessionAuthenticationStrategy"); + List strategies = getFieldValue(strategy, "delegateStrategies"); + return getFieldValue(strategies.get(0), "sessionRegistry"); + } + + private T getFieldValue(Object target, String fieldName) { + try { + return (T) FieldUtils.getFieldValue(target, fieldName); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static SessionResultMatcher session() { + return new SessionResultMatcher(); + } + + private static class SessionResultMatcher implements ResultMatcher { + private String id; + private Boolean valid; + private Boolean exists = true; + + public ResultMatcher exists(boolean exists) { + this.exists = exists; + return this; + } + + public ResultMatcher valid(boolean valid) { + this.valid = valid; + return this.exists(true); + } + + public ResultMatcher id(String id) { + this.id = id; + return this.exists(true); + } + + @Override + public void match(MvcResult result) { + if (!this.exists) { + assertThat(result.getRequest().getSession(false)).isNull(); + return; + } + + assertThat(result.getRequest().getSession(false)).isNotNull(); + + MockHttpSession session = (MockHttpSession) result.getRequest().getSession(false); + + if (this.valid != null) { + if (this.valid) { + assertThat(session.isInvalid()).isFalse(); + } else { + assertThat(session.isInvalid()).isTrue(); + } + } + + if (this.id != null) { + assertThat(session.getId()).isEqualTo(this.id); + } + } + } + + private static MockHttpServletResponse request( + MockHttpServletRequest request, + ApplicationContext context) + throws IOException, ServletException { + + MockHttpServletResponse response = new MockHttpServletResponse(); + + FilterChainProxy proxy = context.getBean(FilterChainProxy.class); + + proxy.doFilter( + request, + new EncodeUrlDenyingHttpServletResponseWrapper(response), + (req, resp) -> {}); + + return response; + } + + private static class EncodeUrlDenyingHttpServletResponseWrapper + extends HttpServletResponseWrapper { + + public EncodeUrlDenyingHttpServletResponseWrapper(HttpServletResponse response) { + super(response); + } + + @Override + public String encodeURL(String url) { + throw new RuntimeException("Unexpected invocation of encodeURL"); + } + + @Override + public String encodeRedirectURL(String url) { + throw new RuntimeException("Unexpected invocation of encodeURL"); + } + + @Override + public String encodeUrl(String url) { + throw new RuntimeException("Unexpected invocation of encodeURL"); + } + + @Override + public String encodeRedirectUrl(String url) { + throw new RuntimeException("Unexpected invocation of encodeURL"); + } + } + + private MockHttpSession expiredSession() { + MockHttpSession session = new MockHttpSession(); + SessionRegistry sessionRegistry = this.spring.getContext().getBean(SessionRegistry.class); + sessionRegistry.registerNewSession(session.getId(), "user"); + sessionRegistry.getSessionInformation(session.getId()).expireNow(); + return session; + } + + private T getFilter(Class filterClass) { + return (T) getFilters().stream() + .filter(filterClass::isInstance) + .findFirst() + .orElse(null); + } + + private List getFilters() { + FilterChainProxy proxy = this.spring.getContext().getBean(FilterChainProxy.class); + + return proxy.getFilters("/"); + } + + private ServletContext servletContext() { + WebApplicationContext context = (WebApplicationContext) this.spring.getContext(); + return context.getServletContext(); + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } +} diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlCustomLogoutHandler.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlCustomLogoutHandler.xml new file mode 100644 index 0000000000..a95022eeac --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlCustomLogoutHandler.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlExpiredUrl.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlExpiredUrl.xml new file mode 100644 index 0000000000..38141dfc7f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlExpiredUrl.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlLogoutAndRememberMeHandlers.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlLogoutAndRememberMeHandlers.xml new file mode 100644 index 0000000000..6f75ce1d2f --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlLogoutAndRememberMeHandlers.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlMaxSessions.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlMaxSessions.xml new file mode 100644 index 0000000000..f16c2da2f8 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlMaxSessions.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlRememberMeHandler.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlRememberMeHandler.xml new file mode 100644 index 0000000000..eec4a40f9b --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlRememberMeHandler.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlSessionRegistryAlias.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlSessionRegistryAlias.xml new file mode 100644 index 0000000000..13baba1949 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlSessionRegistryAlias.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlSessionRegistryRef.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlSessionRegistryRef.xml new file mode 100644 index 0000000000..057f5985c0 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-ConcurrencyControlSessionRegistryRef.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionAlways.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionAlways.xml new file mode 100644 index 0000000000..049c3c4750 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionAlways.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionIfRequired.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionIfRequired.xml new file mode 100644 index 0000000000..47a770d00e --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionIfRequired.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionNever.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionNever.xml new file mode 100644 index 0000000000..0edce4d62a --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionNever.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionStateless.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionStateless.xml new file mode 100644 index 0000000000..b252e1f8f2 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-CreateSessionStateless.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-NoSessionManagementFilter.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-NoSessionManagementFilter.xml new file mode 100644 index 0000000000..84944d4675 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-NoSessionManagementFilter.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-Sec1208.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-Sec1208.xml new file mode 100644 index 0000000000..afdedc0520 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-Sec1208.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-Sec2137.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-Sec2137.xml new file mode 100644 index 0000000000..10fd3e2d5b --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-Sec2137.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionAuthenticationStrategyRef.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionAuthenticationStrategyRef.xml new file mode 100644 index 0000000000..04c2d7a039 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionAuthenticationStrategyRef.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionMigrateSession.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionMigrateSession.xml new file mode 100644 index 0000000000..e0cb047682 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionMigrateSession.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionNone.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionNone.xml new file mode 100644 index 0000000000..217daddcb7 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionNone.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionNoneWithInvalidSessionUrl.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionNoneWithInvalidSessionUrl.xml new file mode 100644 index 0000000000..807cea0710 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTests-SessionFixationProtectionNoneWithInvalidSessionUrl.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + +