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 extends Authentication> anonymousClass = AnonymousAuthenticationToken.class;
+ private Class extends Authentication> firstOfMultiFactorClass = FirstOfMultiFactorAuthenticationToken.class;
private Class extends Authentication> rememberMeClass = RememberMeAuthenticationToken.class;
// ~ Methods
@@ -43,11 +44,25 @@ Class extends Authentication> getAnonymousClass() {
return anonymousClass;
}
+ Class extends Authentication> getFirstOfMultiFactorClass() { return firstOfMultiFactorClass; }
+
Class extends Authentication> 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 extends Authentication> anonymousClass) {
this.anonymousClass = anonymousClass;
}
+ public void setFirstOfMultiFactorClass(Class extends Authentication> firstOfMultiFactorClass) {this.firstOfMultiFactorClass = firstOfMultiFactorClass; }
+
public void setRememberMeClass(Class extends Authentication> 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 extends GrantedAuthority> 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)