diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTransientAuthenticationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTransientAuthenticationTests.java new file mode 100644 index 0000000000..673b59ee83 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/SessionManagementConfigurerTransientAuthenticationTests.java @@ -0,0 +1,123 @@ +/* + * 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.annotation.web.configurers; + +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.AuthenticationProvider; +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; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.TransientAuthentication; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; + +/** + * @author Josh Cummings + */ +public class SessionManagementConfigurerTransientAuthenticationTests { + + @Autowired + MockMvc mvc; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Test + public void postWhenTransientAuthenticationThenNoSessionCreated() + throws Exception { + + this.spring.register(WithTransientAuthenticationConfig.class).autowire(); + MvcResult result = this.mvc.perform(post("/login")).andReturn(); + assertThat(result.getRequest().getSession(false)).isNull(); + } + + @Test + public void postWhenTransientAuthenticationThenAlwaysSessionOverrides() + throws Exception { + + this.spring.register(AlwaysCreateSessionConfig.class).autowire(); + MvcResult result = this.mvc.perform(post("/login")).andReturn(); + assertThat(result.getRequest().getSession(false)).isNotNull(); + } + + @EnableWebSecurity + static class WithTransientAuthenticationConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + super.configure(http); + + http + .csrf().disable(); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth + .authenticationProvider(new TransientAuthenticationProvider()); + } + } + + @EnableWebSecurity + static class AlwaysCreateSessionConfig extends WithTransientAuthenticationConfig { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS); + } + } + + static class TransientAuthenticationProvider implements AuthenticationProvider { + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + return new SomeTransientAuthentication(); + } + + @Override + public boolean supports(Class authentication) { + return true; + } + } + + @TransientAuthentication + static class SomeTransientAuthentication extends AbstractAuthenticationToken { + SomeTransientAuthentication() { + super(null); + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return null; + } + } +} diff --git a/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests.java b/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests.java new file mode 100644 index 0000000000..2bf529bb2d --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests.java @@ -0,0 +1,98 @@ +/* + * 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 org.junit.Rule; +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.TransientAuthentication; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; + +/** + * @author Josh Cummings + */ +public class SessionManagementConfigTransientAuthenticationTests { + private static final String CONFIG_LOCATION_PREFIX = + "classpath:org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests"; + + @Autowired + MockMvc mvc; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Test + public void postWhenTransientAuthenticationThenNoSessionCreated() + throws Exception { + + this.spring.configLocations(this.xml("WithTransientAuthentication")).autowire(); + MvcResult result = this.mvc.perform(post("/login")).andReturn(); + assertThat(result.getRequest().getSession(false)).isNull(); + } + + @Test + public void postWhenTransientAuthenticationThenAlwaysSessionOverrides() + throws Exception { + + this.spring.configLocations(this.xml("CreateSessionAlwaysWithTransientAuthentication")).autowire(); + MvcResult result = this.mvc.perform(post("/login")).andReturn(); + assertThat(result.getRequest().getSession(false)).isNotNull(); + } + + static class TransientAuthenticationProvider implements AuthenticationProvider { + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + return new SomeTransientAuthentication(); + } + + @Override + public boolean supports(Class authentication) { + return true; + } + } + + @TransientAuthentication + static class SomeTransientAuthentication extends AbstractAuthenticationToken { + SomeTransientAuthentication() { + super(null); + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return null; + } + } + + private String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } +} diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests-CreateSessionAlwaysWithTransientAuthentication.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests-CreateSessionAlwaysWithTransientAuthentication.xml new file mode 100644 index 0000000000..804e3dedd4 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests-CreateSessionAlwaysWithTransientAuthentication.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests-WithTransientAuthentication.xml b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests-WithTransientAuthentication.xml new file mode 100644 index 0000000000..ae66dcbbb0 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/SessionManagementConfigTransientAuthenticationTests-WithTransientAuthentication.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/core/src/main/java/org/springframework/security/core/TransientAuthentication.java b/core/src/main/java/org/springframework/security/core/TransientAuthentication.java new file mode 100644 index 0000000000..997ab1dcd4 --- /dev/null +++ b/core/src/main/java/org/springframework/security/core/TransientAuthentication.java @@ -0,0 +1,38 @@ +/* + * 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.core; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A marker for {@link Authentication}s that should never be stored across requests, for example + * a bearer token authentication + * + * @author Josh Cummings + * @since 5.1 + */ +@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface TransientAuthentication { +} diff --git a/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java b/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java index 49f541f9d2..c6deefe877 100644 --- a/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java +++ b/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java @@ -25,9 +25,12 @@ import javax.servlet.http.HttpSession; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.AuthenticationTrustResolverImpl; import org.springframework.security.core.Authentication; +import org.springframework.security.core.TransientAuthentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -387,6 +390,10 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo } private HttpSession createNewSessionIfAllowed(SecurityContext context) { + if (isTransientAuthentication(context.getAuthentication())) { + return null; + } + if (httpSessionExistedAtStartOfRequest) { if (logger.isDebugEnabled()) { logger.debug("HttpSession is now null, but was not null at start of request; " @@ -437,6 +444,10 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo } } + private boolean isTransientAuthentication(Authentication authentication) { + return AnnotationUtils.getAnnotation(authentication.getClass(), TransientAuthentication.class) != null; + } + /** * Sets the {@link AuthenticationTrustResolver} to be used. The default is * {@link AuthenticationTrustResolverImpl}. diff --git a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryServlet25Tests.java b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryServlet25Tests.java new file mode 100644 index 0000000000..5f9f4c2812 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryServlet25Tests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2016 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.web.context; + +import javax.servlet.ServletRequest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.powermock.api.mockito.PowerMockito.spy; +import static org.powermock.api.mockito.PowerMockito.when; + +/** + * @author Luke Taylor + * @author Rob Winch + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest({ ClassUtils.class }) +public class HttpSessionSecurityContextRepositoryServlet25Tests { + @Test + public void servlet25Compatability() throws Exception { + spy(ClassUtils.class); + when(ClassUtils.class, "hasMethod", ServletRequest.class, "startAsync", + new Class[] {}).thenReturn(false); + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, + response); + repo.loadContext(holder); + assertThat(holder.getRequest()).isSameAs(request); + } +} diff --git a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java index 1b9c027fb0..4a9b0845cf 100644 --- a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java +++ b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java @@ -16,18 +16,11 @@ package org.springframework.security.web.context; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Matchers.anyBoolean; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.verify; -import static org.powermock.api.mockito.PowerMockito.mock; -import static org.powermock.api.mockito.PowerMockito.spy; -import static org.powermock.api.mockito.PowerMockito.when; -import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY; - +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import javax.servlet.ServletOutputStream; -import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; @@ -36,25 +29,32 @@ import javax.servlet.http.HttpSession; import org.junit.After; import org.junit.Test; -import org.junit.runner.RunWith; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; + import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AuthenticationTrustResolver; import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.TransientAuthentication; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.when; +import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY; /** * @author Luke Taylor * @author Rob Winch */ -@RunWith(PowerMockRunner.class) -@PrepareForTest({ ClassUtils.class }) public class HttpSessionSecurityContextRepositoryTests { private final TestingAuthenticationToken testToken = new TestingAuthenticationToken( @@ -65,20 +65,6 @@ public class HttpSessionSecurityContextRepositoryTests { SecurityContextHolder.clearContext(); } - @Test - public void servlet25Compatability() throws Exception { - spy(ClassUtils.class); - when(ClassUtils.class, "hasMethod", ServletRequest.class, "startAsync", - new Class[] {}).thenReturn(false); - HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); - MockHttpServletRequest request = new MockHttpServletRequest(); - MockHttpServletResponse response = new MockHttpServletResponse(); - HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, - response); - repo.loadContext(holder); - assertThat(holder.getRequest()).isSameAs(request); - } - @Test public void startAsyncDisablesSaveOnCommit() throws Exception { HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); @@ -633,4 +619,102 @@ public class HttpSessionSecurityContextRepositoryTests { repo.saveContext(context, request, response); } + + @Test + public void saveContextWhenTransientAuthenticationThenSkipped() { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, + response); + SecurityContext context = repo.loadContext(holder); + + SomeTransientAuthentication authentication = new SomeTransientAuthentication(); + context.setAuthentication(authentication); + + repo.saveContext(context, holder.getRequest(), holder.getResponse()); + + MockHttpSession session = (MockHttpSession) request.getSession(false); + assertThat(session).isNull(); + } + + @Test + public void saveContextWhenTransientAuthenticationSubclassThenSkipped() { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, + response); + SecurityContext context = repo.loadContext(holder); + + SomeTransientAuthenticationSubclass authentication = new SomeTransientAuthenticationSubclass(); + context.setAuthentication(authentication); + + repo.saveContext(context, holder.getRequest(), holder.getResponse()); + + MockHttpSession session = (MockHttpSession) request.getSession(false); + assertThat(session).isNull(); + } + + @Test + public void saveContextWhenTransientAuthenticationWithCustomAnnotationThenSkipped() { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, + response); + SecurityContext context = repo.loadContext(holder); + + SomeOtherTransientAuthentication authentication = new SomeOtherTransientAuthentication(); + context.setAuthentication(authentication); + + repo.saveContext(context, holder.getRequest(), holder.getResponse()); + + MockHttpSession session = (MockHttpSession) request.getSession(false); + assertThat(session).isNull(); + } + + @TransientAuthentication + private static class SomeTransientAuthentication extends AbstractAuthenticationToken { + public SomeTransientAuthentication() { + super(null); + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return null; + } + } + + private static class SomeTransientAuthenticationSubclass extends SomeTransientAuthentication { + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @TransientAuthentication + public @interface TestTransientAuthentication { + } + + @TestTransientAuthentication + private static class SomeOtherTransientAuthentication extends AbstractAuthenticationToken { + public SomeOtherTransientAuthentication() { + super(null); + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return null; + } + } }