Skip to content

Commit fc630ae

Browse files
committed
Merge branch '6.5.x' into 7.0.x
2 parents 88118af + a317a3d commit fc630ae

2 files changed

Lines changed: 81 additions & 10 deletions

File tree

core/src/main/java/org/springframework/security/authentication/dao/AbstractUserDetailsAuthenticationProvider.java

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ public abstract class AbstractUserDetailsAuthenticationProvider
9797

9898
private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
9999

100+
private boolean alwaysPerformAdditionalChecksOnUser = true;
101+
100102
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
101103

102104
private static final String AUTHORITY = FactorGrantedAuthority.PASSWORD_AUTHORITY;
@@ -154,8 +156,7 @@ public Authentication authenticate(Authentication authentication) throws Authent
154156
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
155157
}
156158
try {
157-
this.preAuthenticationChecks.check(user);
158-
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
159+
performPreCheck(user, (UsernamePasswordAuthenticationToken) authentication);
159160
}
160161
catch (AuthenticationException ex) {
161162
if (!cacheWasUsed) {
@@ -165,8 +166,7 @@ public Authentication authenticate(Authentication authentication) throws Authent
165166
// we're using latest data (i.e. not from the cache)
166167
cacheWasUsed = false;
167168
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
168-
this.preAuthenticationChecks.check(user);
169-
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
169+
performPreCheck(user, (UsernamePasswordAuthenticationToken) authentication);
170170
}
171171
this.postAuthenticationChecks.check(user);
172172
if (!cacheWasUsed) {
@@ -179,6 +179,25 @@ public Authentication authenticate(Authentication authentication) throws Authent
179179
return createSuccessAuthentication(principalToReturn, authentication, user);
180180
}
181181

182+
private void performPreCheck(UserDetails user, UsernamePasswordAuthenticationToken authentication) {
183+
try {
184+
this.preAuthenticationChecks.check(user);
185+
}
186+
catch (AuthenticationException ex) {
187+
if (!this.alwaysPerformAdditionalChecksOnUser) {
188+
throw ex;
189+
}
190+
try {
191+
additionalAuthenticationChecks(user, authentication);
192+
}
193+
catch (AuthenticationException ignored) {
194+
// preserve the original failed check
195+
}
196+
throw ex;
197+
}
198+
additionalAuthenticationChecks(user, authentication);
199+
}
200+
182201
private String determineUsername(Authentication authentication) {
183202
return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
184203
}
@@ -324,6 +343,22 @@ public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChe
324343
this.postAuthenticationChecks = postAuthenticationChecks;
325344
}
326345

346+
/**
347+
* Set whether to always perform the additional checks on the user, even if the
348+
* pre-authentication checks fail. This is useful to ensure that regardless of the
349+
* state of the user account, authentication takes the same amount of time to
350+
* complete.
351+
*
352+
* <p>
353+
* For applications that rely on the additional checks running only once should set
354+
* this value to {@code false}
355+
* @param alwaysPerformAdditionalChecksOnUser
356+
* @since 5.7.23
357+
*/
358+
public void setAlwaysPerformAdditionalChecksOnUser(boolean alwaysPerformAdditionalChecksOnUser) {
359+
this.alwaysPerformAdditionalChecksOnUser = alwaysPerformAdditionalChecksOnUser;
360+
}
361+
327362
public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
328363
this.authoritiesMapper = authoritiesMapper;
329364
}

core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.List;
2222

2323
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
2425

2526
import org.springframework.cache.Cache;
2627
import org.springframework.dao.DataRetrievalFailureException;
@@ -44,6 +45,7 @@
4445
import org.springframework.security.core.userdetails.PasswordEncodedUser;
4546
import org.springframework.security.core.userdetails.User;
4647
import org.springframework.security.core.userdetails.UserDetails;
48+
import org.springframework.security.core.userdetails.UserDetailsChecker;
4749
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
4850
import org.springframework.security.core.userdetails.UserDetailsService;
4951
import org.springframework.security.core.userdetails.UsernameNotFoundException;
@@ -64,6 +66,7 @@
6466
import static org.mockito.ArgumentMatchers.eq;
6567
import static org.mockito.ArgumentMatchers.isA;
6668
import static org.mockito.BDDMockito.given;
69+
import static org.mockito.BDDMockito.willThrow;
6770
import static org.mockito.Mockito.mock;
6871
import static org.mockito.Mockito.times;
6972
import static org.mockito.Mockito.verify;
@@ -422,12 +425,10 @@ public void constructWhenPasswordEncoderProvidedThenSets() {
422425
assertThat(daoAuthenticationProvider.getPasswordEncoder()).isSameAs(NoOpPasswordEncoder.getInstance());
423426
}
424427

425-
/**
426-
* This is an explicit test for SEC-2056. It is intentionally ignored since this test
427-
* is not deterministic and {@link #testUserNotFoundEncodesPassword()} ensures that
428-
* SEC-2056 is fixed.
429-
*/
430-
public void IGNOREtestSec2056() {
428+
// SEC-2056
429+
@Test
430+
@EnabledIfSystemProperty(named = "spring.security.timing-tests", matches = "true")
431+
public void testSec2056() {
431432
UsernamePasswordAuthenticationToken foundUser = UsernamePasswordAuthenticationToken.unauthenticated("rod",
432433
"koala");
433434
UsernamePasswordAuthenticationToken notFoundUser = UsernamePasswordAuthenticationToken
@@ -460,6 +461,41 @@ public void IGNOREtestSec2056() {
460461
.isTrue();
461462
}
462463

464+
// related to SEC-2056
465+
@Test
466+
@EnabledIfSystemProperty(named = "spring.security.timing-tests", matches = "true")
467+
public void testDisabledUserTiming() {
468+
UsernamePasswordAuthenticationToken user = UsernamePasswordAuthenticationToken.unauthenticated("rod", "koala");
469+
PasswordEncoder encoder = new BCryptPasswordEncoder();
470+
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
471+
provider.setPasswordEncoder(encoder);
472+
MockUserDetailsServiceUserRod users = new MockUserDetailsServiceUserRod();
473+
users.password = encoder.encode((CharSequence) user.getCredentials());
474+
provider.setUserDetailsService(users);
475+
int sampleSize = 100;
476+
List<Long> enabledTimes = new ArrayList<>(sampleSize);
477+
for (int i = 0; i < sampleSize; i++) {
478+
long start = System.currentTimeMillis();
479+
provider.authenticate(user);
480+
enabledTimes.add(System.currentTimeMillis() - start);
481+
}
482+
UserDetailsChecker preChecks = mock(UserDetailsChecker.class);
483+
willThrow(new DisabledException("User is disabled")).given(preChecks).check(any(UserDetails.class));
484+
provider.setPreAuthenticationChecks(preChecks);
485+
List<Long> disabledTimes = new ArrayList<>(sampleSize);
486+
for (int i = 0; i < sampleSize; i++) {
487+
long start = System.currentTimeMillis();
488+
assertThatExceptionOfType(DisabledException.class).isThrownBy(() -> provider.authenticate(user));
489+
disabledTimes.add(System.currentTimeMillis() - start);
490+
}
491+
double enabledAvg = avg(enabledTimes);
492+
double disabledAvg = avg(disabledTimes);
493+
assertThat(Math.abs(disabledAvg - enabledAvg) <= 3)
494+
.withFailMessage("Disabled user average " + disabledAvg + " should be within 3ms of enabled user average "
495+
+ enabledAvg)
496+
.isTrue();
497+
}
498+
463499
private double avg(List<Long> counts) {
464500
return counts.stream().mapToLong(Long::longValue).average().orElse(0);
465501
}

0 commit comments

Comments
 (0)