/*
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* 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.dao;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.encoding.ShaPasswordEncoder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.userdetails.cache.EhCacheBasedUserCache;
import org.springframework.security.core.userdetails.cache.NullUserCache;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* Tests {@link DaoAuthenticationProvider}.
*
* @author Ben Alex
* @author Rob Winch
*/
public class DaoAuthenticationProviderTests {
private static final List<GrantedAuthority> ROLES_12 = AuthorityUtils.createAuthorityList(
"ROLE_ONE", "ROLE_TWO");
// ~ Methods
// ========================================================================================================
@Test
public void testAuthenticateFailsForIncorrectPasswordCase() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
"rod", "KOala");
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(new MockAuthenticationDaoUserrod());
provider.setUserCache(new MockUserCache());
try {
provider.authenticate(token);
fail("Should have thrown BadCredentialsException");
}
catch (BadCredentialsException expected) {
}
}
@Test
public void testReceivedBadCredentialsWhenCredentialsNotProvided() {
// Test related to SEC-434
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(new MockAuthenticationDaoUserrod());
provider.setUserCache(new MockUserCache());
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
"rod", null);
try {
provider.authenticate(authenticationToken);
fail("Expected BadCredenialsException");
}
catch (BadCredentialsException expected) {
}
}
@Test
public void testAuthenticateFailsIfAccountExpired() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
"peter", "opal");
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(
new MockAuthenticationDaoUserPeterAccountExpired());
provider.setUserCache(new MockUserCache());
try {
provider.authenticate(token);
fail("Should have thrown AccountExpiredException");
}
catch (AccountExpiredException expected) {
}
}
@Test
public void testAuthenticateFailsIfAccountLocked() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
"peter", "opal");
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(new MockAuthenticationDaoUserPeterAccountLocked());
provider.setUserCache(new MockUserCache());
try {
provider.authenticate(token);
fail("Should have thrown LockedException");
}
catch (LockedException expected) {
}
}
@Test
public void testAuthenticateFailsIfCredentialsExpired() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
"peter", "opal");
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(
new MockAuthenticationDaoUserPeterCredentialsExpired());
provider.setUserCache(new MockUserCache());
try {
provider.authenticate(token);
fail("Should have thrown CredentialsExpiredException");
}
catch (CredentialsExpiredException expected) {
}
// Check that wrong password causes BadCredentialsException, rather than
// CredentialsExpiredException
token = new UsernamePasswordAuthenticationToken("peter", "wrong_password");
try {
provider.authenticate(token);
fail("Should have thrown BadCredentialsException");
}
catch (BadCredentialsException expected) {
}
}
@Test
public void testAuthenticateFailsIfUserDisabled() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
"peter", "opal");
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(new MockAuthenticationDaoUserPeter());
provider.setUserCache(new MockUserCache());
try {
provider.authenticate(token);
fail("Should have thrown DisabledException");
}
catch (DisabledException expected) {
}
}
@Test
public void testAuthenticateFailsWhenAuthenticationDaoHasBackendFailure() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
"rod", "koala");
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(new MockAuthenticationDaoSimulateBackendError());
provider.setUserCache(new MockUserCache());
try {
provider.authenticate(token);
fail("Should have thrown InternalAuthenticationServiceException");
}
catch (InternalAuthenticationServiceException expected) {
}
}
@Test
public void testAuthenticateFailsWithEmptyUsername() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
null, "koala");
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(new MockAuthenticationDaoUserrod());
provider.setUserCache(new MockUserCache());
try {
provider.authenticate(token);
fail("Should have thrown BadCredentialsException");
}
catch (BadCredentialsException expected) {
}
}
@Test
public void testAuthenticateFailsWithInvalidPassword() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
"rod", "INVALID_PASSWORD");
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(new MockAuthenticationDaoUserrod());
provider.setUserCache(new MockUserCache());
try {
provider.authenticate(token);
fail("Should have thrown BadCredentialsException");
}
catch (BadCredentialsException expected) {
}
}
@Test
public void testAuthenticateFailsWithInvalidUsernameAndHideUserNotFoundExceptionFalse() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
"INVALID_USER", "koala");
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setHideUserNotFoundExceptions(false); // we want
// UsernameNotFoundExceptions
provider.setUserDetailsService(new MockAuthenticationDaoUserrod());
provider.setUserCache(new MockUserCache());
try {
provider.authenticate(token);
fail("Should have thrown UsernameNotFoundException");
}
catch (UsernameNotFoundException expected) {
}
}
@Test
public void testAuthenticateFailsWithInvalidUsernameAndHideUserNotFoundExceptionsWithDefaultOfTrue() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
"INVALID_USER", "koala");
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
assertThat(provider.isHideUserNotFoundExceptions()).isTrue();
provider.setUserDetailsService(new MockAuthenticationDaoUserrod());
provider.setUserCache(new MockUserCache());
try {
provider.authenticate(token);
fail("Should have thrown BadCredentialsException");
}
catch (BadCredentialsException expected) {
}
}
@Test
public void testAuthenticateFailsWithMixedCaseUsernameIfDefaultChanged() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
"RoD", "koala");
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(new MockAuthenticationDaoUserrod());
provider.setUserCache(new MockUserCache());
try {
provider.authenticate(token);
fail("Should have thrown BadCredentialsException");
}
catch (BadCredentialsException expected) {
}
}
@Test
public void testAuthenticates() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
"rod", "koala");
token.setDetails("192.168.0.1");
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(new MockAuthenticationDaoUserrod());
provider.setUserCache(new MockUserCache());
Authentication result = provider.authenticate(token);
if (!(result instanceof UsernamePasswordAuthenticationToken)) {
fail("Should have returned instance of UsernamePasswordAuthenticationToken");
}
UsernamePasswordAuthenticationToken castResult = (UsernamePasswordAuthenticationToken) result;
assertThat(castResult.getPrincipal().getClass()).isEqualTo(User.class);
assertThat(castResult.getCredentials()).isEqualTo("koala");
assertThat(
AuthorityUtils.authorityListToSet(castResult.getAuthorities())).contains(
"ROLE_ONE", "ROLE_TWO");
assertThat(castResult.getDetails()).isEqualTo("192.168.0.1");
}
@Test
public void testAuthenticatesASecondTime() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
"rod", "koala");
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(new MockAuthenticationDaoUserrod());
provider.setUserCache(new MockUserCache());
Authentication result = provider.authenticate(token);
if (!(result instanceof UsernamePasswordAuthenticationToken)) {
fail("Should have returned instance of UsernamePasswordAuthenticationToken");
}
// Now try to authenticate with the previous result (with its UserDetails)
Authentication result2 = provider.authenticate(result);
if (!(result2 instanceof UsernamePasswordAuthenticationToken)) {
fail("Should have returned instance of UsernamePasswordAuthenticationToken");
}
assertThat(result2.getCredentials()).isEqualTo(result.getCredentials());
}
@Test
public void testAuthenticatesWhenASaltIsUsed() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
"rod", "koala");
SystemWideSaltSource salt = new SystemWideSaltSource();
salt.setSystemWideSalt("SYSTEM_SALT_VALUE");
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(new MockAuthenticationDaoUserrodWithSalt());
provider.setSaltSource(salt);
provider.setUserCache(new MockUserCache());
Authentication result = provider.authenticate(token);
if (!(result instanceof UsernamePasswordAuthenticationToken)) {
fail("Should have returned instance of UsernamePasswordAuthenticationToken");
}
assertThat(result.getPrincipal().getClass()).isEqualTo(User.class);
// We expect original credentials user submitted to be returned
assertThat(result.getCredentials()).isEqualTo("koala");
assertThat(AuthorityUtils.authorityListToSet(result.getAuthorities())).contains(
"ROLE_ONE", "ROLE_TWO");
}
@Test
public void testAuthenticatesWithForcePrincipalAsString() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
"rod", "koala");
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(new MockAuthenticationDaoUserrod());
provider.setUserCache(new MockUserCache());
provider.setForcePrincipalAsString(true);
Authentication result = provider.authenticate(token);
if (!(result instanceof UsernamePasswordAuthenticationToken)) {
fail("Should have returned instance of UsernamePasswordAuthenticationToken");
}
UsernamePasswordAuthenticationToken castResult = (UsernamePasswordAuthenticationToken) result;
assertThat(castResult.getPrincipal().getClass()).isEqualTo(String.class);
assertThat(castResult.getPrincipal()).isEqualTo("rod");
}
@Test
public void testDetectsNullBeingReturnedFromAuthenticationDao() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
"rod", "koala");
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(new MockAuthenticationDaoReturnsNull());
try {
provider.authenticate(token);
fail("Should have thrown AuthenticationServiceException");
}
catch (AuthenticationServiceException expected) {
assertThat(
"UserDetailsService returned null, which is an interface contract violation").isEqualTo(
expected.getMessage());
}
}
@Test
public void testGettersSetters() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(new ShaPasswordEncoder());
assertThat(provider.getPasswordEncoder().getClass()).isEqualTo(
ShaPasswordEncoder.class);
provider.setSaltSource(new SystemWideSaltSource());
assertThat(provider.getSaltSource().getClass()).isEqualTo(
SystemWideSaltSource.class);
provider.setUserCache(new EhCacheBasedUserCache());
assertThat(provider.getUserCache().getClass()).isEqualTo(
EhCacheBasedUserCache.class);
assertThat(provider.isForcePrincipalAsString()).isFalse();
provider.setForcePrincipalAsString(true);
assertThat(provider.isForcePrincipalAsString()).isTrue();
}
@Test
public void testGoesBackToAuthenticationDaoToObtainLatestPasswordIfCachedPasswordSeemsIncorrect() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
"rod", "koala");
MockAuthenticationDaoUserrod authenticationDao = new MockAuthenticationDaoUserrod();
MockUserCache cache = new MockUserCache();
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(authenticationDao);
provider.setUserCache(cache);
// This will work, as password still "koala"
provider.authenticate(token);
// Check "rod = koala" ended up in the cache
assertThat(cache.getUserFromCache("rod").getPassword()).isEqualTo("koala");
// Now change the password the AuthenticationDao will return
authenticationDao.setPassword("easternLongNeckTurtle");
// Now try authentication again, with the new password
token = new UsernamePasswordAuthenticationToken("rod", "easternLongNeckTurtle");
provider.authenticate(token);
// To get this far, the new password was accepted
// Check the cache was updated
assertThat(cache.getUserFromCache("rod").getPassword()).isEqualTo(
"easternLongNeckTurtle");
}
@Test
public void testStartupFailsIfNoAuthenticationDao() throws Exception {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
try {
provider.afterPropertiesSet();
fail("Should have thrown IllegalArgumentException");
}
catch (IllegalArgumentException expected) {
}
}
@Test
public void testStartupFailsIfNoUserCacheSet() throws Exception {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(new MockAuthenticationDaoUserrod());
assertThat(provider.getUserCache().getClass()).isEqualTo(NullUserCache.class);
provider.setUserCache(null);
try {
provider.afterPropertiesSet();
fail("Should have thrown IllegalArgumentException");
}
catch (IllegalArgumentException expected) {
}
}
@Test
public void testStartupSuccess() throws Exception {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
UserDetailsService userDetailsService = new MockAuthenticationDaoUserrod();
provider.setUserDetailsService(userDetailsService);
provider.setUserCache(new MockUserCache());
assertThat(provider.getUserDetailsService()).isEqualTo(userDetailsService);
provider.afterPropertiesSet();
}
@Test
public void testSupports() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
assertThat(provider.supports(UsernamePasswordAuthenticationToken.class)).isTrue();
assertThat(!provider.supports(TestingAuthenticationToken.class)).isTrue();
}
// SEC-2056
@Test
public void testUserNotFoundEncodesPassword() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
"missing", "koala");
PasswordEncoder encoder = mock(PasswordEncoder.class);
when(encoder.encode(anyString())).thenReturn("koala");
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setHideUserNotFoundExceptions(false);
provider.setPasswordEncoder(encoder);
provider.setUserDetailsService(new MockAuthenticationDaoUserrod());
try {
provider.authenticate(token);
fail("Expected Exception");
}
catch (UsernameNotFoundException success) {
}
// ensure encoder invoked w/ non-null strings since PasswordEncoder impls may fail
// if encoded password is null
verify(encoder).matches(isA(String.class), isA(String.class));
}
@Test
public void testUserNotFoundBCryptPasswordEncoder() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
"missing", "koala");
PasswordEncoder encoder = new BCryptPasswordEncoder();
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setHideUserNotFoundExceptions(false);
provider.setPasswordEncoder(encoder);
MockAuthenticationDaoUserrod userDetailsService = new MockAuthenticationDaoUserrod();
userDetailsService.password = encoder.encode(
(CharSequence) token.getCredentials());
provider.setUserDetailsService(userDetailsService);
try {
provider.authenticate(token);
fail("Expected Exception");
}
catch (UsernameNotFoundException success) {
}
}
@Test
public void testUserNotFoundDefaultEncoder() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
"missing", null);
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setHideUserNotFoundExceptions(false);
provider.setUserDetailsService(new MockAuthenticationDaoUserrod());
try {
provider.authenticate(token);
fail("Expected Exception");
}
catch (UsernameNotFoundException success) {
}
}
/**
* This is an explicit test for SEC-2056. It is intentionally ignored since this test
* is not deterministic and {@link #testUserNotFoundEncodesPassword()} ensures that
* SEC-2056 is fixed.
*/
public void IGNOREtestSec2056() {
UsernamePasswordAuthenticationToken foundUser = new UsernamePasswordAuthenticationToken(
"rod", "koala");
UsernamePasswordAuthenticationToken notFoundUser = new UsernamePasswordAuthenticationToken(
"notFound", "koala");
PasswordEncoder encoder = new BCryptPasswordEncoder(10, new SecureRandom());
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setHideUserNotFoundExceptions(false);
provider.setPasswordEncoder(encoder);
MockAuthenticationDaoUserrod userDetailsService = new MockAuthenticationDaoUserrod();
userDetailsService.password = encoder.encode(
(CharSequence) foundUser.getCredentials());
provider.setUserDetailsService(userDetailsService);
int sampleSize = 100;
List<Long> userFoundTimes = new ArrayList<Long>(sampleSize);
for (int i = 0; i < sampleSize; i++) {
long start = System.currentTimeMillis();
provider.authenticate(foundUser);
userFoundTimes.add(System.currentTimeMillis() - start);
}
List<Long> userNotFoundTimes = new ArrayList<Long>(sampleSize);
for (int i = 0; i < sampleSize; i++) {
long start = System.currentTimeMillis();
try {
provider.authenticate(notFoundUser);
fail("Expected Exception");
}
catch (UsernameNotFoundException success) {
}
userNotFoundTimes.add(System.currentTimeMillis() - start);
}
double userFoundAvg = avg(userFoundTimes);
double userNotFoundAvg = avg(userNotFoundTimes);
assertThat(Math.abs(userNotFoundAvg - userFoundAvg) <= 3).withFailMessage(
"User not found average " + userNotFoundAvg
+ " should be within 3ms of user found average "
+ userFoundAvg).isTrue();
}
private double avg(List<Long> counts) {
long sum = 0;
for (Long time : counts) {
sum += time;
}
return sum / counts.size();
}
@Test
public void testUserNotFoundNullCredentials() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
"missing", null);
PasswordEncoder encoder = mock(PasswordEncoder.class);
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setHideUserNotFoundExceptions(false);
provider.setPasswordEncoder(encoder);
provider.setUserDetailsService(new MockAuthenticationDaoUserrod());
try {
provider.authenticate(token);
fail("Expected Exception");
}
catch (UsernameNotFoundException success) {
}
verify(encoder, times(0)).matches(anyString(), anyString());
}
// ~ Inner Classes
// ==================================================================================================
private class MockAuthenticationDaoReturnsNull implements UserDetailsService {
public UserDetails loadUserByUsername(String username) {
return null;
}
}
private class MockAuthenticationDaoSimulateBackendError
implements UserDetailsService {
public UserDetails loadUserByUsername(String username) {
throw new DataRetrievalFailureException(
"This mock simulator is designed to fail");
}
}
private class MockAuthenticationDaoUserrod implements UserDetailsService {
private String password = "koala";
public UserDetails loadUserByUsername(String username) {
if ("rod".equals(username)) {
return new User("rod", password, true, true, true, true, ROLES_12);
}
else {
throw new UsernameNotFoundException("Could not find: " + username);
}
}
public void setPassword(String password) {
this.password = password;
}
}
private class MockAuthenticationDaoUserrodWithSalt implements UserDetailsService {
public UserDetails loadUserByUsername(String username) {
if ("rod".equals(username)) {
return new User("rod", "koala{SYSTEM_SALT_VALUE}", true, true, true, true,
ROLES_12);
}
else {
throw new UsernameNotFoundException("Could not find: " + username);
}
}
}
private class MockAuthenticationDaoUserPeter implements UserDetailsService {
public UserDetails loadUserByUsername(String username) {
if ("peter".equals(username)) {
return new User("peter", "opal", false, true, true, true, ROLES_12);
}
else {
throw new UsernameNotFoundException("Could not find: " + username);
}
}
}
private class MockAuthenticationDaoUserPeterAccountExpired
implements UserDetailsService {
public UserDetails loadUserByUsername(String username) {
if ("peter".equals(username)) {
return new User("peter", "opal", true, false, true, true, ROLES_12);
}
else {
throw new UsernameNotFoundException("Could not find: " + username);
}
}
}
private class MockAuthenticationDaoUserPeterAccountLocked
implements UserDetailsService {
public UserDetails loadUserByUsername(String username) {
if ("peter".equals(username)) {
return new User("peter", "opal", true, true, true, false, ROLES_12);
}
else {
throw new UsernameNotFoundException("Could not find: " + username);
}
}
}
private class MockAuthenticationDaoUserPeterCredentialsExpired
implements UserDetailsService {
public UserDetails loadUserByUsername(String username) {
if ("peter".equals(username)) {
return new User("peter", "opal", true, true, false, true, ROLES_12);
}
else {
throw new UsernameNotFoundException("Could not find: " + username);
}
}
}
}