Skip to content

Commit 188107a

Browse files
committed
Add FirstOfMultifactorAuthenticationToken
To indicate the state of principal which passed first step of multi step (factor) authehntication
1 parent 6095340 commit 188107a

File tree

9 files changed

+183
-7
lines changed

9 files changed

+183
-7
lines changed

core/src/main/java/org/springframework/security/authentication/AuthenticationTrustResolver.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,27 @@ public interface AuthenticationTrustResolver {
3939
* will always return <code>false</code>)
4040
*
4141
* @return <code>true</code> the passed authentication token represented an anonymous
42-
* principal, <code>false</code> otherwise
42+
* principal or in the middle of multi factor authentication process,
43+
* <code>false</code> otherwise
4344
*/
4445
boolean isAnonymous(Authentication authentication);
4546

47+
48+
/**
49+
* Indicates whether the passed <code>Authentication</code> token represents a
50+
* fully anonymous user (not authenticated and also not in the middle of multi factor
51+
* authentication process.
52+
* The method is provided to distinguish fully anonymous principal from the principal
53+
* which has passed the first step of multi step (factor) authentication.
54+
*
55+
* @param authentication to test (may be <code>null</code> in which case the method
56+
* will always return <code>false</code>)
57+
*
58+
* @return <code>true</code> the passed authentication token represented an anonymous
59+
* principal, <code>false</code> otherwise
60+
*/
61+
boolean isFullyAnonymous(Authentication authentication);
62+
4663
/**
4764
* Indicates whether the passed <code>Authentication</code> token represents user that
4865
* has been remembered (i.e. not a user that has been fully authenticated).

core/src/main/java/org/springframework/security/authentication/AuthenticationTrustResolverImpl.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public class AuthenticationTrustResolverImpl implements AuthenticationTrustResol
3434
// ================================================================================================
3535

3636
private Class<? extends Authentication> anonymousClass = AnonymousAuthenticationToken.class;
37+
private Class<? extends Authentication> firstOfMultiFactorClass = FirstOfMultiFactorAuthenticationToken.class;
3738
private Class<? extends Authentication> rememberMeClass = RememberMeAuthenticationToken.class;
3839

3940
// ~ Methods
@@ -43,11 +44,25 @@ Class<? extends Authentication> getAnonymousClass() {
4344
return anonymousClass;
4445
}
4546

47+
Class<? extends Authentication> getFirstOfMultiFactorClass() { return firstOfMultiFactorClass; }
48+
4649
Class<? extends Authentication> getRememberMeClass() {
4750
return rememberMeClass;
4851
}
4952

5053
public boolean isAnonymous(Authentication authentication) {
54+
if(isFullyAnonymous(authentication)){
55+
return true;
56+
}
57+
if ((firstOfMultiFactorClass == null) || (authentication == null)) {
58+
return false;
59+
}
60+
61+
return firstOfMultiFactorClass.isAssignableFrom(authentication.getClass());
62+
}
63+
64+
@Override
65+
public boolean isFullyAnonymous(Authentication authentication) {
5166
if ((anonymousClass == null) || (authentication == null)) {
5267
return false;
5368
}
@@ -67,6 +82,8 @@ public void setAnonymousClass(Class<? extends Authentication> anonymousClass) {
6782
this.anonymousClass = anonymousClass;
6883
}
6984

85+
public void setFirstOfMultiFactorClass(Class<? extends Authentication> firstOfMultiFactorClass) {this.firstOfMultiFactorClass = firstOfMultiFactorClass; }
86+
7087
public void setRememberMeClass(Class<? extends Authentication> rememberMeClass) {
7188
this.rememberMeClass = rememberMeClass;
7289
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org.springframework.security.authentication;
2+
3+
import org.springframework.security.core.GrantedAuthority;
4+
import org.springframework.security.core.SpringSecurityCoreVersion;
5+
6+
import java.util.Collection;
7+
8+
public class FirstOfMultiFactorAuthenticationToken extends AbstractAuthenticationToken {
9+
10+
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
11+
12+
private Object principal;
13+
private Object credentials;
14+
15+
// ~ Constructors
16+
// ===================================================================================================
17+
public FirstOfMultiFactorAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
18+
super(authorities);
19+
this.principal = principal;
20+
this.credentials = credentials;
21+
setAuthenticated(true);
22+
}
23+
24+
// ~ Methods
25+
// ========================================================================================================
26+
27+
@Override
28+
public Object getPrincipal() {
29+
return principal;
30+
}
31+
32+
@Override
33+
public Object getCredentials() {
34+
return credentials;
35+
}
36+
37+
@Override
38+
public void eraseCredentials() {
39+
super.eraseCredentials();
40+
credentials = null;
41+
}
42+
43+
}

core/src/test/java/org/springframework/security/authentication/AuthenticationTrustResolverImplTests.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,23 @@ public void testCorrectOperationIsAnonymous() {
3636
AuthenticationTrustResolverImpl trustResolver = new AuthenticationTrustResolverImpl();
3737
assertThat(trustResolver.isAnonymous(new AnonymousAuthenticationToken("ignored",
3838
"ignored", AuthorityUtils.createAuthorityList("ignored")))).isTrue();
39+
assertThat(trustResolver.isAnonymous(new FirstOfMultiFactorAuthenticationToken("ignored",
40+
"ignored", AuthorityUtils.createAuthorityList("ignored")))).isTrue();
3941
assertThat(trustResolver.isAnonymous(new TestingAuthenticationToken("ignored",
4042
"ignored", AuthorityUtils.createAuthorityList("ignored")))).isFalse();
4143
}
4244

45+
@Test
46+
public void testCorrectOperationIsFullyAnonymous() {
47+
AuthenticationTrustResolverImpl trustResolver = new AuthenticationTrustResolverImpl();
48+
assertThat(trustResolver.isFullyAnonymous(new AnonymousAuthenticationToken("ignored",
49+
"ignored", AuthorityUtils.createAuthorityList("ignored")))).isTrue();
50+
assertThat(trustResolver.isFullyAnonymous(new FirstOfMultiFactorAuthenticationToken("ignored",
51+
"ignored", AuthorityUtils.createAuthorityList("ignored")))).isFalse();
52+
assertThat(trustResolver.isFullyAnonymous(new TestingAuthenticationToken("ignored",
53+
"ignored", AuthorityUtils.createAuthorityList("ignored")))).isFalse();
54+
}
55+
4356
@Test
4457
public void testCorrectOperationIsRememberMe() {
4558
AuthenticationTrustResolverImpl trustResolver = new AuthenticationTrustResolverImpl();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.springframework.security.authentication;
2+
3+
import org.junit.Test;
4+
import org.springframework.security.core.authority.AuthorityUtils;
5+
6+
import static org.assertj.core.api.Assertions.assertThat;
7+
8+
public class FirstOfMultiFactorAuthenticationTokenTests {
9+
// ~ Methods
10+
// ========================================================================================================
11+
12+
@Test
13+
public void authenticatedPropertyContractIsSatisfied() {
14+
FirstOfMultiFactorAuthenticationToken token = new FirstOfMultiFactorAuthenticationToken(
15+
"Test", "Password", AuthorityUtils.NO_AUTHORITIES);
16+
17+
// check default given we passed some GrantedAuthority[]s (well, we passed empty
18+
// list)
19+
assertThat(token.isAuthenticated()).isTrue();
20+
21+
// check explicit set to untrusted (we can safely go from trusted to untrusted,
22+
// but not the reverse)
23+
token.setAuthenticated(false);
24+
assertThat(token.isAuthenticated()).isFalse();
25+
26+
}
27+
28+
@Test
29+
public void gettersReturnCorrectData() {
30+
FirstOfMultiFactorAuthenticationToken token = new FirstOfMultiFactorAuthenticationToken(
31+
"Test", "Password",
32+
AuthorityUtils.createAuthorityList("ROLE_ONE", "ROLE_TWO"));
33+
assertThat(token.getPrincipal()).isEqualTo("Test");
34+
assertThat(token.getCredentials()).isEqualTo("Password");
35+
assertThat(AuthorityUtils.authorityListToSet(token.getAuthorities())).contains("ROLE_ONE");
36+
assertThat(AuthorityUtils.authorityListToSet(token.getAuthorities())).contains("ROLE_TWO");
37+
}
38+
39+
@Test(expected = NoSuchMethodException.class)
40+
public void testNoArgConstructorDoesntExist() throws Exception {
41+
Class<?> clazz = UsernamePasswordAuthenticationToken.class;
42+
clazz.getDeclaredConstructor((Class[]) null);
43+
}
44+
45+
}

web/src/main/java/org/springframework/security/web/access/ExceptionTranslationFilter.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,15 @@ else if (exception instanceof AccessDeniedException) {
202202
protected void sendStartAuthentication(HttpServletRequest request,
203203
HttpServletResponse response, FilterChain chain,
204204
AuthenticationException reason) throws ServletException, IOException {
205-
// SEC-112: Clear the SecurityContextHolder's Authentication, as the
206-
// existing Authentication is no longer considered valid
207-
SecurityContextHolder.getContext().setAuthentication(null);
205+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
206+
if (authenticationTrustResolver.isAnonymous(authentication) && !authenticationTrustResolver.isFullyAnonymous(authentication)) {
207+
// no-op if in the middle of multi step authentication
208+
}
209+
else {
210+
// SEC-112: Clear the SecurityContextHolder's Authentication, as the
211+
// existing Authentication is no longer considered valid
212+
SecurityContextHolder.getContext().setAuthentication(null);
213+
}
208214
requestCache.saveRequest(request, response);
209215
logger.debug("Calling Authentication entry point.");
210216
authenticationEntryPoint.commence(request, response, reason);

web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ final class SaveToSessionResponseWrapper extends
332332
/**
333333
* Stores the supplied security context in the session (if available) and if it
334334
* has changed since it was set at the start of the request. If the
335-
* AuthenticationTrustResolver identifies the current user as anonymous, then the
335+
* AuthenticationTrustResolver identifies the current user as fully anonymous, then the
336336
* context will not be stored.
337337
*
338338
* @param context the context object obtained from the SecurityContextHolder after
@@ -347,7 +347,7 @@ protected void saveContext(SecurityContext context) {
347347
HttpSession httpSession = request.getSession(false);
348348

349349
// See SEC-776
350-
if (authentication == null || trustResolver.isAnonymous(authentication)) {
350+
if (authentication == null || trustResolver.isFullyAnonymous(authentication)) {
351351
if (logger.isDebugEnabled()) {
352352
logger.debug("SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.");
353353
}

web/src/test/java/org/springframework/security/web/access/ExceptionTranslationFilterTests.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.springframework.security.authentication.AnonymousAuthenticationToken;
2727
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
2828
import org.springframework.security.authentication.BadCredentialsException;
29+
import org.springframework.security.authentication.FirstOfMultiFactorAuthenticationToken;
2930
import org.springframework.security.authentication.RememberMeAuthenticationToken;
3031
import org.springframework.security.core.AuthenticationException;
3132
import org.springframework.security.core.authority.AuthorityUtils;
@@ -109,6 +110,40 @@ public void testAccessDeniedWhenAnonymous() throws Exception {
109110
assertThat(getSavedRequestUrl(request)).isEqualTo("http://www.example.com/mycontext/secure/page.html");
110111
}
111112

113+
@Test
114+
public void testAccessDeniedWhenFirstOfMultiFactorAuthentication() throws Exception {
115+
// Setup our HTTP request
116+
MockHttpServletRequest request = new MockHttpServletRequest();
117+
request.setServletPath("/secure/page.html");
118+
request.setServerPort(80);
119+
request.setScheme("http");
120+
request.setServerName("www.example.com");
121+
request.setContextPath("/mycontext");
122+
request.setRequestURI("/mycontext/secure/page.html");
123+
124+
// Setup the FilterChain to thrown an access denied exception
125+
FilterChain fc = mock(FilterChain.class);
126+
doThrow(new AccessDeniedException("")).when(fc).doFilter(
127+
any(HttpServletRequest.class), any(HttpServletResponse.class));
128+
129+
// Setup SecurityContextHolder, as filter needs to check if user is
130+
// anonymous
131+
SecurityContextHolder.getContext().setAuthentication(
132+
new FirstOfMultiFactorAuthenticationToken("ignored", "ignored", AuthorityUtils
133+
.createAuthorityList("IGNORED")));
134+
135+
// Test
136+
ExceptionTranslationFilter filter = new ExceptionTranslationFilter(mockEntryPoint);
137+
filter.setAuthenticationTrustResolver(new AuthenticationTrustResolverImpl());
138+
assertThat(filter.getAuthenticationTrustResolver()).isNotNull();
139+
140+
MockHttpServletResponse response = new MockHttpServletResponse();
141+
filter.doFilter(request, response, fc);
142+
assertThat(response.getRedirectedUrl()).isEqualTo("/mycontext/login.jsp");
143+
assertThat(getSavedRequestUrl(request)).isEqualTo("http://www.example.com/mycontext/secure/page.html");
144+
}
145+
146+
112147
@Test
113148
public void testAccessDeniedWithRememberMe() throws Exception {
114149
// Setup our HTTP request

web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,7 @@ public void saveContextCustomTrustResolver() {
592592

593593
repo.saveContext(contextToSave, holder.getRequest(), holder.getResponse());
594594

595-
verify(trustResolver).isAnonymous(contextToSave.getAuthentication());
595+
verify(trustResolver).isFullyAnonymous(contextToSave.getAuthentication());
596596
}
597597

598598
@Test(expected = IllegalArgumentException.class)

0 commit comments

Comments
 (0)