diff --git a/core/src/main/java/org/springframework/security/authentication/AuthenticationTrustResolver.java b/core/src/main/java/org/springframework/security/authentication/AuthenticationTrustResolver.java index e0f2a59f66c..a092bd8216c 100644 --- a/core/src/main/java/org/springframework/security/authentication/AuthenticationTrustResolver.java +++ b/core/src/main/java/org/springframework/security/authentication/AuthenticationTrustResolver.java @@ -39,10 +39,27 @@ public interface AuthenticationTrustResolver { * will always return false) * * @return true the passed authentication token represented an anonymous - * principal, false otherwise + * principal or in the middle of multi factor authentication process, + * false otherwise */ boolean isAnonymous(Authentication authentication); + + /** + * Indicates whether the passed Authentication token represents a + * fully anonymous user (not authenticated and also not in the middle of multi factor + * authentication process. + * The method is provided to distinguish fully anonymous principal from the principal + * which has passed the first step of multi step (factor) authentication. + * + * @param authentication to test (may be null in which case the method + * will always return false) + * + * @return true the passed authentication token represented an anonymous + * principal, false otherwise + */ + boolean isFullyAnonymous(Authentication authentication); + /** * Indicates whether the passed Authentication token represents user that * has been remembered (i.e. not a user that has been fully authenticated). diff --git a/core/src/main/java/org/springframework/security/authentication/AuthenticationTrustResolverImpl.java b/core/src/main/java/org/springframework/security/authentication/AuthenticationTrustResolverImpl.java index 65ee8bafbfd..18e89995e4d 100644 --- a/core/src/main/java/org/springframework/security/authentication/AuthenticationTrustResolverImpl.java +++ b/core/src/main/java/org/springframework/security/authentication/AuthenticationTrustResolverImpl.java @@ -34,6 +34,7 @@ public class AuthenticationTrustResolverImpl implements AuthenticationTrustResol // ================================================================================================ private Class anonymousClass = AnonymousAuthenticationToken.class; + private Class firstOfMultiFactorClass = FirstOfMultiFactorAuthenticationToken.class; private Class rememberMeClass = RememberMeAuthenticationToken.class; // ~ Methods @@ -43,11 +44,25 @@ Class getAnonymousClass() { return anonymousClass; } + Class getFirstOfMultiFactorClass() { return firstOfMultiFactorClass; } + Class getRememberMeClass() { return rememberMeClass; } public boolean isAnonymous(Authentication authentication) { + if(isFullyAnonymous(authentication)){ + return true; + } + if ((firstOfMultiFactorClass == null) || (authentication == null)) { + return false; + } + + return firstOfMultiFactorClass.isAssignableFrom(authentication.getClass()); + } + + @Override + public boolean isFullyAnonymous(Authentication authentication) { if ((anonymousClass == null) || (authentication == null)) { return false; } @@ -67,6 +82,8 @@ public void setAnonymousClass(Class anonymousClass) { this.anonymousClass = anonymousClass; } + public void setFirstOfMultiFactorClass(Class firstOfMultiFactorClass) {this.firstOfMultiFactorClass = firstOfMultiFactorClass; } + public void setRememberMeClass(Class rememberMeClass) { this.rememberMeClass = rememberMeClass; } diff --git a/core/src/main/java/org/springframework/security/authentication/FirstOfMultiFactorAuthenticationToken.java b/core/src/main/java/org/springframework/security/authentication/FirstOfMultiFactorAuthenticationToken.java new file mode 100644 index 00000000000..ce9e1d7b369 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authentication/FirstOfMultiFactorAuthenticationToken.java @@ -0,0 +1,59 @@ +/* + * 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.authentication; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.SpringSecurityCoreVersion; + +import java.util.Collection; + +public class FirstOfMultiFactorAuthenticationToken extends AbstractAuthenticationToken { + + private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; + + private Object principal; + private Object credentials; + + // ~ Constructors + // =================================================================================================== + public FirstOfMultiFactorAuthenticationToken(Object principal, Object credentials, Collection authorities) { + super(authorities); + this.principal = principal; + this.credentials = credentials; + setAuthenticated(true); + } + + // ~ Methods + // ======================================================================================================== + + @Override + public Object getPrincipal() { + return principal; + } + + @Override + public Object getCredentials() { + return credentials; + } + + @Override + public void eraseCredentials() { + super.eraseCredentials(); + credentials = null; + } + +} diff --git a/core/src/test/java/org/springframework/security/authentication/AuthenticationTrustResolverImplTests.java b/core/src/test/java/org/springframework/security/authentication/AuthenticationTrustResolverImplTests.java index 908fee22602..8c9a9272a3a 100644 --- a/core/src/test/java/org/springframework/security/authentication/AuthenticationTrustResolverImplTests.java +++ b/core/src/test/java/org/springframework/security/authentication/AuthenticationTrustResolverImplTests.java @@ -36,10 +36,23 @@ public void testCorrectOperationIsAnonymous() { AuthenticationTrustResolverImpl trustResolver = new AuthenticationTrustResolverImpl(); assertThat(trustResolver.isAnonymous(new AnonymousAuthenticationToken("ignored", "ignored", AuthorityUtils.createAuthorityList("ignored")))).isTrue(); + assertThat(trustResolver.isAnonymous(new FirstOfMultiFactorAuthenticationToken("ignored", + "ignored", AuthorityUtils.createAuthorityList("ignored")))).isTrue(); assertThat(trustResolver.isAnonymous(new TestingAuthenticationToken("ignored", "ignored", AuthorityUtils.createAuthorityList("ignored")))).isFalse(); } + @Test + public void testCorrectOperationIsFullyAnonymous() { + AuthenticationTrustResolverImpl trustResolver = new AuthenticationTrustResolverImpl(); + assertThat(trustResolver.isFullyAnonymous(new AnonymousAuthenticationToken("ignored", + "ignored", AuthorityUtils.createAuthorityList("ignored")))).isTrue(); + assertThat(trustResolver.isFullyAnonymous(new FirstOfMultiFactorAuthenticationToken("ignored", + "ignored", AuthorityUtils.createAuthorityList("ignored")))).isFalse(); + assertThat(trustResolver.isFullyAnonymous(new TestingAuthenticationToken("ignored", + "ignored", AuthorityUtils.createAuthorityList("ignored")))).isFalse(); + } + @Test public void testCorrectOperationIsRememberMe() { AuthenticationTrustResolverImpl trustResolver = new AuthenticationTrustResolverImpl(); diff --git a/core/src/test/java/org/springframework/security/authentication/FirstOfMultiFactorAuthenticationTokenTests.java b/core/src/test/java/org/springframework/security/authentication/FirstOfMultiFactorAuthenticationTokenTests.java new file mode 100644 index 00000000000..321061cb28b --- /dev/null +++ b/core/src/test/java/org/springframework/security/authentication/FirstOfMultiFactorAuthenticationTokenTests.java @@ -0,0 +1,61 @@ +/* + * 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.authentication; + +import org.junit.Test; +import org.springframework.security.core.authority.AuthorityUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +public class FirstOfMultiFactorAuthenticationTokenTests { + // ~ Methods + // ======================================================================================================== + + @Test + public void authenticatedPropertyContractIsSatisfied() { + FirstOfMultiFactorAuthenticationToken token = new FirstOfMultiFactorAuthenticationToken( + "Test", "Password", AuthorityUtils.NO_AUTHORITIES); + + // check default given we passed some GrantedAuthority[]s (well, we passed empty + // list) + assertThat(token.isAuthenticated()).isTrue(); + + // check explicit set to untrusted (we can safely go from trusted to untrusted, + // but not the reverse) + token.setAuthenticated(false); + assertThat(token.isAuthenticated()).isFalse(); + + } + + @Test + public void gettersReturnCorrectData() { + FirstOfMultiFactorAuthenticationToken token = new FirstOfMultiFactorAuthenticationToken( + "Test", "Password", + AuthorityUtils.createAuthorityList("ROLE_ONE", "ROLE_TWO")); + assertThat(token.getPrincipal()).isEqualTo("Test"); + assertThat(token.getCredentials()).isEqualTo("Password"); + assertThat(AuthorityUtils.authorityListToSet(token.getAuthorities())).contains("ROLE_ONE"); + assertThat(AuthorityUtils.authorityListToSet(token.getAuthorities())).contains("ROLE_TWO"); + } + + @Test(expected = NoSuchMethodException.class) + public void testNoArgConstructorDoesntExist() throws Exception { + Class clazz = UsernamePasswordAuthenticationToken.class; + clazz.getDeclaredConstructor((Class[]) null); + } + +} diff --git a/web/src/main/java/org/springframework/security/web/access/ExceptionTranslationFilter.java b/web/src/main/java/org/springframework/security/web/access/ExceptionTranslationFilter.java index 0fb87f73d92..bea1991cdd9 100644 --- a/web/src/main/java/org/springframework/security/web/access/ExceptionTranslationFilter.java +++ b/web/src/main/java/org/springframework/security/web/access/ExceptionTranslationFilter.java @@ -202,9 +202,15 @@ else if (exception instanceof AccessDeniedException) { protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException { - // SEC-112: Clear the SecurityContextHolder's Authentication, as the - // existing Authentication is no longer considered valid - SecurityContextHolder.getContext().setAuthentication(null); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authenticationTrustResolver.isAnonymous(authentication) && !authenticationTrustResolver.isFullyAnonymous(authentication)) { + // no-op if in the middle of multi step authentication + } + else { + // SEC-112: Clear the SecurityContextHolder's Authentication, as the + // existing Authentication is no longer considered valid + SecurityContextHolder.getContext().setAuthentication(null); + } requestCache.saveRequest(request, response); logger.debug("Calling Authentication entry point."); authenticationEntryPoint.commence(request, response, reason); 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 49f541f9d2b..46ac6e756ec 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 @@ -332,7 +332,7 @@ final class SaveToSessionResponseWrapper extends /** * Stores the supplied security context in the session (if available) and if it * has changed since it was set at the start of the request. If the - * AuthenticationTrustResolver identifies the current user as anonymous, then the + * AuthenticationTrustResolver identifies the current user as fully anonymous, then the * context will not be stored. * * @param context the context object obtained from the SecurityContextHolder after @@ -347,7 +347,7 @@ protected void saveContext(SecurityContext context) { HttpSession httpSession = request.getSession(false); // See SEC-776 - if (authentication == null || trustResolver.isAnonymous(authentication)) { + if (authentication == null || trustResolver.isFullyAnonymous(authentication)) { if (logger.isDebugEnabled()) { logger.debug("SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession."); } diff --git a/web/src/test/java/org/springframework/security/web/access/ExceptionTranslationFilterTests.java b/web/src/test/java/org/springframework/security/web/access/ExceptionTranslationFilterTests.java index 5602c71a2c7..71f48b43e23 100644 --- a/web/src/test/java/org/springframework/security/web/access/ExceptionTranslationFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/access/ExceptionTranslationFilterTests.java @@ -26,6 +26,7 @@ import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AuthenticationTrustResolverImpl; import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.FirstOfMultiFactorAuthenticationToken; import org.springframework.security.authentication.RememberMeAuthenticationToken; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.AuthorityUtils; @@ -109,6 +110,40 @@ public void testAccessDeniedWhenAnonymous() throws Exception { assertThat(getSavedRequestUrl(request)).isEqualTo("http://www.example.com/mycontext/secure/page.html"); } + @Test + public void testAccessDeniedWhenFirstOfMultiFactorAuthentication() throws Exception { + // Setup our HTTP request + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setServletPath("/secure/page.html"); + request.setServerPort(80); + request.setScheme("http"); + request.setServerName("www.example.com"); + request.setContextPath("/mycontext"); + request.setRequestURI("/mycontext/secure/page.html"); + + // Setup the FilterChain to thrown an access denied exception + FilterChain fc = mock(FilterChain.class); + doThrow(new AccessDeniedException("")).when(fc).doFilter( + any(HttpServletRequest.class), any(HttpServletResponse.class)); + + // Setup SecurityContextHolder, as filter needs to check if user is + // anonymous + SecurityContextHolder.getContext().setAuthentication( + new FirstOfMultiFactorAuthenticationToken("ignored", "ignored", AuthorityUtils + .createAuthorityList("IGNORED"))); + + // Test + ExceptionTranslationFilter filter = new ExceptionTranslationFilter(mockEntryPoint); + filter.setAuthenticationTrustResolver(new AuthenticationTrustResolverImpl()); + assertThat(filter.getAuthenticationTrustResolver()).isNotNull(); + + MockHttpServletResponse response = new MockHttpServletResponse(); + filter.doFilter(request, response, fc); + assertThat(response.getRedirectedUrl()).isEqualTo("/mycontext/login.jsp"); + assertThat(getSavedRequestUrl(request)).isEqualTo("http://www.example.com/mycontext/secure/page.html"); + } + + @Test public void testAccessDeniedWithRememberMe() throws Exception { // Setup our HTTP 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 1b9c027fb01..1b640b4a64e 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 @@ -592,7 +592,7 @@ public void saveContextCustomTrustResolver() { repo.saveContext(contextToSave, holder.getRequest(), holder.getResponse()); - verify(trustResolver).isAnonymous(contextToSave.getAuthentication()); + verify(trustResolver).isFullyAnonymous(contextToSave.getAuthentication()); } @Test(expected = IllegalArgumentException.class)