diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.groovy deleted file mode 100644 index 9fc3e70a3f..0000000000 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.groovy +++ /dev/null @@ -1,229 +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 - * - * https://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.annotation.web.configurers - -import org.springframework.context.ApplicationListener -import org.springframework.context.annotation.Bean -import org.springframework.mock.web.MockHttpSession -import org.springframework.security.authentication.TestingAuthenticationToken -import org.springframework.security.config.annotation.BaseSpringSpec -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.core.session.SessionRegistry -import org.springframework.security.web.authentication.session.AbstractSessionFixationProtectionStrategy; -import org.springframework.security.web.authentication.session.ChangeSessionIdAuthenticationStrategy; -import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy -import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy -import org.springframework.security.web.authentication.session.SessionFixationProtectionEvent -import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy -import org.springframework.security.web.session.ConcurrentSessionFilter -import org.springframework.security.web.session.SessionManagementFilter -import org.springframework.security.web.session.InvalidSessionStrategy - -/** - * - * @author Rob Winch - */ -class NamespaceSessionManagementTests extends BaseSpringSpec { - - def "http/session-management"() { - when: - loadConfig(SessionManagementConfig) - then: - findSessionAuthenticationStrategy(AbstractSessionFixationProtectionStrategy) - } - - @EnableWebSecurity - static class SessionManagementConfig extends WebSecurityConfigurerAdapter { - @Override - protected void configure(HttpSecurity http) throws Exception { - // enabled by default - } - } - - def "http/session-management custom"() { - setup: - CustomSessionManagementConfig.SR = Mock(SessionRegistry) - when: - loadConfig(CustomSessionManagementConfig) - def concurrentStrategy = findFilter(SessionManagementFilter).sessionAuthenticationStrategy.delegateStrategies[0] - then: - findFilter(SessionManagementFilter).invalidSessionStrategy.destinationUrl == "/invalid-session" - findFilter(SessionManagementFilter).failureHandler.defaultFailureUrl == "/session-auth-error" - concurrentStrategy.maximumSessions == 1 - concurrentStrategy.exceptionIfMaximumExceeded - concurrentStrategy.sessionRegistry == CustomSessionManagementConfig.SR - findFilter(ConcurrentSessionFilter).sessionInformationExpiredStrategy.destinationUrl == "/expired-session" - } - - @EnableWebSecurity - static class CustomSessionManagementConfig extends WebSecurityConfigurerAdapter { - static SessionRegistry SR - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .sessionManagement() - .invalidSessionUrl("/invalid-session") // session-management@invalid-session-url - .sessionAuthenticationErrorUrl("/session-auth-error") // session-management@session-authentication-error-url - .maximumSessions(1) // session-management/concurrency-control@max-sessions - .maxSessionsPreventsLogin(true) // session-management/concurrency-control@error-if-maximum-exceeded - .expiredUrl("/expired-session") // session-management/concurrency-control@expired-url - .sessionRegistry(SR) // session-management/concurrency-control@session-registry-ref - } - } - - // gh-3371 - def "http/session-management custom invalidationstrategy"() { - setup: - InvalidSessionStrategyConfig.ISS = Mock(InvalidSessionStrategy) - when: - loadConfig(InvalidSessionStrategyConfig) - then: - findFilter(SessionManagementFilter).invalidSessionStrategy == InvalidSessionStrategyConfig.ISS - } - - @EnableWebSecurity - static class InvalidSessionStrategyConfig extends WebSecurityConfigurerAdapter { - static InvalidSessionStrategy ISS - - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .sessionManagement() - .invalidSessionStrategy(ISS) - } - } - - def "http/session-management refs"() { - setup: - RefsSessionManagementConfig.SAS = Mock(SessionAuthenticationStrategy) - when: - loadConfig(RefsSessionManagementConfig) - then: - findFilter(SessionManagementFilter).sessionAuthenticationStrategy.delegateStrategies.find { it == RefsSessionManagementConfig.SAS } - } - - @EnableWebSecurity - static class RefsSessionManagementConfig extends WebSecurityConfigurerAdapter { - static SessionAuthenticationStrategy SAS - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .sessionManagement() - .sessionAuthenticationStrategy(SAS) // session-management@session-authentication-strategy-ref - } - } - - def "http/session-management@session-fixation-protection=none"() { - when: - loadConfig(SFPNoneSessionManagementConfig) - then: - findFilter(SessionManagementFilter).sessionAuthenticationStrategy.delegateStrategies.find { it instanceof NullAuthenticatedSessionStrategy } - } - - @EnableWebSecurity - static class SFPNoneSessionManagementConfig extends WebSecurityConfigurerAdapter { - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .sessionManagement() - .sessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy()) - } - } - - def "http/session-management@session-fixation-protection=migrateSession (default)"() { - when: - loadConfig(SFPMigrateSessionManagementConfig) - then: - if(isChangeSession()) { - findSessionAuthenticationStrategy(ChangeSessionIdAuthenticationStrategy) - } else { - findSessionAuthenticationStrategy(SessionFixationProtectionStrategy).migrateSessionAttributes - } - } - - @EnableWebSecurity - static class SFPMigrateSessionManagementConfig extends WebSecurityConfigurerAdapter { - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .sessionManagement() - } - } - - def "SEC-2913: Default JavaConfig session fixation AuthenticationStrategy has NullEventPublisher"() { - setup: - loadConfig(SFPPostProcessedConfig) - when: - findSessionAuthenticationStrategy(AbstractSessionFixationProtectionStrategy).onSessionChange("id", new MockHttpSession(), new TestingAuthenticationToken("u","p","ROLE_USER")) - then: - context.getBean(MockEventListener).events - } - - @EnableWebSecurity - static class SFPPostProcessedConfig extends WebSecurityConfigurerAdapter { - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .sessionManagement() - } - - @Bean - public MockEventListener eventListener() { - new MockEventListener() - } - } - - def "http/session-management@session-fixation-protection=newSession"() { - when: - loadConfig(SFPNewSessionSessionManagementConfig) - then: - !findSessionAuthenticationStrategy(SessionFixationProtectionStrategy).migrateSessionAttributes - } - - def findSessionAuthenticationStrategy(def c) { - findFilter(SessionManagementFilter).sessionAuthenticationStrategy.delegateStrategies.find { c.isAssignableFrom(it.class) } - } - - @EnableWebSecurity - static class SFPNewSessionSessionManagementConfig extends WebSecurityConfigurerAdapter { - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .sessionManagement() - .sessionFixation() - .newSession() - } - } - - static class MockEventListener implements ApplicationListener { - List events = [] - - public void onApplicationEvent(SessionFixationProtectionEvent event) { - events.add(event) - } - - } - - boolean isChangeSession() { - try { - new ChangeSessionIdAuthenticationStrategy() - return true - } catch(Exception e) {} - return false - } -} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.java new file mode 100644 index 0000000000..e3a1f6bde0 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/NamespaceSessionManagementTests.java @@ -0,0 +1,471 @@ +/* + * Copyright 2002-2019 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 + * + * https://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.annotation.web.configurers; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpSession; +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.config.test.SpringTestRule; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.session.SessionInformation; +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.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy; +import org.springframework.security.web.authentication.session.SessionAuthenticationException; +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; +import org.springframework.security.web.authentication.session.SessionFixationProtectionEvent; +import org.springframework.security.web.session.InvalidSessionStrategy; +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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * + * @author Rob Winch + * @author Josh Cummings + */ +public class NamespaceSessionManagementTests { + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + MockMvc mvc; + + @Test + public void authenticateWhenDefaultSessionManagementThenMatchesNamespace() throws Exception { + this.spring.register + (SessionManagementConfig.class, BasicController.class, UserDetailsServiceConfig.class).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); + } + + @EnableWebSecurity + static class SessionManagementConfig extends WebSecurityConfigurerAdapter { + } + + @Test + public void authenticateWhenUsingInvalidSessionUrlThenMatchesNamespace() throws Exception { + this.spring.register(CustomSessionManagementConfig.class).autowire(); + + this.mvc.perform(get("/auth") + .with(request -> { + request.setRequestedSessionIdValid(false); + request.setRequestedSessionId("id"); + return request; + })) + .andExpect(redirectedUrl("/invalid-session")); + } + + + @Test + public void authenticateWhenUsingExpiredUrlThenMatchesNamespace() throws Exception { + this.spring.register(CustomSessionManagementConfig.class).autowire(); + + MockHttpSession session = new MockHttpSession(); + SessionInformation sessionInformation = new SessionInformation(new Object(), session.getId(), new Date(0)); + sessionInformation.expireNow(); + SessionRegistry sessionRegistry = this.spring.getContext().getBean(SessionRegistry.class); + when(sessionRegistry.getSessionInformation(session.getId())).thenReturn(sessionInformation); + + this.mvc.perform(get("/auth").session(session)) + .andExpect(redirectedUrl("/expired-session")); + } + + @Test + public void authenticateWhenUsingMaxSessionsThenMatchesNamespace() throws Exception { + this.spring.register(CustomSessionManagementConfig.class, BasicController.class, UserDetailsServiceConfig.class).autowire(); + + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(redirectedUrl("/session-auth-error")); + } + + @Test + public void authenticateWhenUsingFailureUrlThenMatchesNamespace() throws Exception { + this.spring.register(CustomSessionManagementConfig.class, BasicController.class, UserDetailsServiceConfig.class).autowire(); + + MockHttpServletRequest mock = spy(MockHttpServletRequest.class); + mock.setSession(new MockHttpSession()); + when(mock.changeSessionId()).thenThrow(SessionAuthenticationException.class); + mock.setMethod("GET"); + + this.mvc.perform(get("/auth") + .with(request -> mock) + .with(httpBasic("user", "password"))) + .andExpect(redirectedUrl("/session-auth-error")); + } + + @Test + public void authenticateWhenUsingSessionRegistryThenMatchesNamespace() throws Exception { + this.spring.register(CustomSessionManagementConfig.class, BasicController.class, UserDetailsServiceConfig.class).autowire(); + + SessionRegistry sessionRegistry = this.spring.getContext().getBean(SessionRegistry.class); + + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + verify(sessionRegistry).registerNewSession(any(String.class), any(Object.class)); + } + + @EnableWebSecurity + static class CustomSessionManagementConfig extends WebSecurityConfigurerAdapter { + SessionRegistry sessionRegistry = spy(SessionRegistryImpl.class); + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests() + .anyRequest().authenticated() + .and() + .httpBasic() + .and() + .sessionManagement() + .invalidSessionUrl("/invalid-session") // session-management@invalid-session-url + .sessionAuthenticationErrorUrl("/session-auth-error") // session-management@session-authentication-error-url + .maximumSessions(1) // session-management/concurrency-control@max-sessions + .maxSessionsPreventsLogin(true) // session-management/concurrency-control@error-if-maximum-exceeded + .expiredUrl("/expired-session") // session-management/concurrency-control@expired-url + .sessionRegistry(sessionRegistry()); // session-management/concurrency-control@session-registry-ref + } + + @Bean + SessionRegistry sessionRegistry() { + return this.sessionRegistry; + } + } + + + // gh-3371 + @Test + public void authenticateWhenUsingCustomInvalidSessionStrategyThenMatchesNamespace() throws Exception { + this.spring.register(InvalidSessionStrategyConfig.class).autowire(); + + this.mvc.perform(get("/auth") + .with(request -> { + request.setRequestedSessionIdValid(false); + request.setRequestedSessionId("id"); + return request; + })) + .andExpect(status().isOk()); + + verifyBean(InvalidSessionStrategy.class) + .onInvalidSessionDetected(any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + + @EnableWebSecurity + static class InvalidSessionStrategyConfig extends WebSecurityConfigurerAdapter { + InvalidSessionStrategy invalidSessionStrategy = mock(InvalidSessionStrategy.class); + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .sessionManagement() + .invalidSessionStrategy(invalidSessionStrategy()); + } + + @Bean + InvalidSessionStrategy invalidSessionStrategy() { + return this.invalidSessionStrategy; + } + } + + @Test + public void authenticateWhenUsingCustomSessionAuthenticationStrategyThenMatchesNamespace() throws Exception { + this.spring.register(RefsSessionManagementConfig.class, BasicController.class, UserDetailsServiceConfig.class).autowire(); + + this.mvc.perform(get("/auth") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()); + + verifyBean(SessionAuthenticationStrategy.class) + .onAuthentication(any(Authentication.class), + any(HttpServletRequest.class), any(HttpServletResponse.class)); + } + + @EnableWebSecurity + static class RefsSessionManagementConfig extends WebSecurityConfigurerAdapter { + SessionAuthenticationStrategy sessionAuthenticationStrategy = + mock(SessionAuthenticationStrategy.class); + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .sessionManagement() + .sessionAuthenticationStrategy(sessionAuthenticationStrategy()) // session-management@session-authentication-strategy-ref + .and() + .httpBasic(); + } + + @Bean + SessionAuthenticationStrategy sessionAuthenticationStrategy() { + return this.sessionAuthenticationStrategy; + } + } + + @Test + public void authenticateWhenNoSessionFixationProtectionThenMatchesNamespace() throws Exception { + this.spring.register(SFPNoneSessionManagementConfig.class, BasicController.class, UserDetailsServiceConfig.class).autowire(); + + MockHttpSession givenSession = new MockHttpSession(); + String givenSessionId = givenSession.getId(); + MockHttpSession resultingSession = (MockHttpSession) + this.mvc.perform(get("/auth") + .session(givenSession) + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()) + .andReturn().getRequest().getSession(false); + + assertThat(givenSessionId).isEqualTo(resultingSession.getId()); + } + + @EnableWebSecurity + static class SFPNoneSessionManagementConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .sessionManagement() + .sessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy()) + .and() + .httpBasic(); + } + } + + @Test + public void authenticateWhenMigrateSessionFixationProtectionThenMatchesNamespace() throws Exception { + this.spring.register(SFPMigrateSessionManagementConfig.class, BasicController.class, UserDetailsServiceConfig.class).autowire(); + + MockHttpSession givenSession = new MockHttpSession(); + String givenSessionId = givenSession.getId(); + givenSession.setAttribute("name", "value"); + + MockHttpSession resultingSession = (MockHttpSession) + this.mvc.perform(get("/auth") + .session(givenSession) + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()) + .andReturn().getRequest().getSession(false); + + assertThat(givenSessionId).isNotEqualTo(resultingSession.getId()); + assertThat(resultingSession.getAttribute("name")).isEqualTo("value"); + } + + @EnableWebSecurity + static class SFPMigrateSessionManagementConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .sessionManagement() + .and() + .httpBasic(); + } + } + + // SEC-2913 + @Test + public void authenticateWhenUsingSessionFixationProtectionThenUsesNonNullEventPublisher() throws Exception { + this.spring.register(SFPPostProcessedConfig.class, UserDetailsServiceConfig.class).autowire(); + + this.mvc.perform(get("/auth") + .session(new MockHttpSession()) + .with(httpBasic("user", "password"))) + .andExpect(status().isNotFound()); + + verifyBean(MockEventListener.class).onApplicationEvent(any(SessionFixationProtectionEvent.class)); + } + + @EnableWebSecurity + static class SFPPostProcessedConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .sessionManagement() + .and() + .httpBasic(); + } + + @Bean + public MockEventListener eventListener() { + return spy(new MockEventListener()); + } + } + + @Test + public void authenticateWhenNewSessionFixationProtectionThenMatchesNamespace() throws Exception { + this.spring.register(SFPNewSessionSessionManagementConfig.class, UserDetailsServiceConfig.class).autowire(); + + MockHttpSession givenSession = new MockHttpSession(); + String givenSessionId = givenSession.getId(); + givenSession.setAttribute("name", "value"); + + MockHttpSession resultingSession = (MockHttpSession) + this.mvc.perform(get("/auth") + .session(givenSession) + .with(httpBasic("user", "password"))) + .andExpect(status().isNotFound()) + .andReturn().getRequest().getSession(false); + + assertThat(givenSessionId).isNotEqualTo(resultingSession.getId()); + assertThat(resultingSession.getAttribute("name")).isNull(); + } + + @EnableWebSecurity + static class SFPNewSessionSessionManagementConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .sessionManagement() + .sessionFixation().newSession() + .and() + .httpBasic(); + } + } + + + private T verifyBean(Class clazz) { + return verify(this.spring.getContext().getBean(clazz)); + } + + static class MockEventListener implements ApplicationListener { + List events = new ArrayList<>(); + + public void onApplicationEvent(SessionFixationProtectionEvent event) { + this.events.add(event); + } + } + + @Configuration + static class UserDetailsServiceConfig { + @Bean + UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager( + User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build()); + } + } + + @RestController + static class BasicController { + @GetMapping("/") + public String ok() { + return "ok"; + } + + @GetMapping("/auth") + public String auth(Principal principal) { + return principal.getName(); + } + } + + 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); + } + } + } +}