Skip to content

Add FirstOfMultifactorAuthenticationToken #5196

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,27 @@ public interface AuthenticationTrustResolver {
* will always return <code>false</code>)
*
* @return <code>true</code> the passed authentication token represented an anonymous
* principal, <code>false</code> otherwise
* principal or in the middle of multi factor authentication process,
* <code>false</code> otherwise
*/
boolean isAnonymous(Authentication authentication);


/**
* Indicates whether the passed <code>Authentication</code> 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 <code>null</code> in which case the method
* will always return <code>false</code>)
*
* @return <code>true</code> the passed authentication token represented an anonymous
* principal, <code>false</code> otherwise
*/
boolean isFullyAnonymous(Authentication authentication);

/**
* Indicates whether the passed <code>Authentication</code> token represents user that
* has been remembered (i.e. not a user that has been fully authenticated).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down