Browse Source

Configuration Support for UserDetailsPasswordManager

Issue: gh-2778
Rob Winch 7 years ago
parent
commit
3ca5810bc8

+ 6 - 1
config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java

@@ -22,6 +22,7 @@ import org.springframework.security.authentication.dao.DaoAuthenticationProvider
 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.core.userdetails.UserDetailsPasswordService;
 
 /**
  * Lazily initializes the global authentication with a {@link UserDetailsService} if it is
@@ -65,12 +66,16 @@ class InitializeUserDetailsBeanManagerConfigurer
 			}
 
 			PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
+			UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class);
 
 			DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
 			provider.setUserDetailsService(userDetailsService);
 			if (passwordEncoder != null) {
 				provider.setPasswordEncoder(passwordEncoder);
 			}
+			if (passwordManager != null) {
+				provider.setUserDetailsPasswordService(passwordManager);
+			}
 			provider.afterPropertiesSet();
 
 			auth.authenticationProvider(provider);
@@ -90,4 +95,4 @@ class InitializeUserDetailsBeanManagerConfigurer
 					.getBean(userDetailsBeanNames[0], type);
 		}
 	}
-}
+}

+ 9 - 0
config/src/main/java/org/springframework/security/config/annotation/authentication/configurers/userdetails/AbstractDaoAuthenticationConfigurer.java

@@ -21,6 +21,7 @@ import org.springframework.security.config.annotation.SecurityBuilder;
 import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.core.userdetails.UserDetailsPasswordService;
 
 /**
  * Allows configuring a {@link DaoAuthenticationProvider}
@@ -46,6 +47,9 @@ abstract class AbstractDaoAuthenticationConfigurer<B extends ProviderManagerBuil
 	protected AbstractDaoAuthenticationConfigurer(U userDetailsService) {
 		this.userDetailsService = userDetailsService;
 		provider.setUserDetailsService(userDetailsService);
+		if (userDetailsService instanceof UserDetailsPasswordService) {
+			this.provider.setUserDetailsPasswordService((UserDetailsPasswordService) userDetailsService);
+		}
 	}
 
 	/**
@@ -73,6 +77,11 @@ abstract class AbstractDaoAuthenticationConfigurer<B extends ProviderManagerBuil
 		return (C) this;
 	}
 
+	public C userDetailsPasswordManager(UserDetailsPasswordService passwordManager) {
+		provider.setUserDetailsPasswordService(passwordManager);
+		return (C) this;
+	}
+
 	@Override
 	public void configure(B builder) throws Exception {
 		provider = postProcess(provider);

+ 32 - 0
config/src/test/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.java

@@ -52,6 +52,7 @@ import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.provisioning.InMemoryUserDetailsManager;
+import org.springframework.security.core.userdetails.UserDetailsPasswordService;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -60,6 +61,8 @@ import java.util.List;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.startsWith;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -395,6 +398,35 @@ public class AuthenticationConfigurationTests {
 		}
 	}
 
+	@Test
+	public void getAuthenticationWhenUserDetailsServiceAndPasswordManagerThenManagerUsed() throws Exception {
+		UserDetails user = new User("user", "{noop}password",
+				AuthorityUtils.createAuthorityList("ROLE_USER"));
+		this.spring.register(UserDetailsPasswordManagerBeanConfig.class).autowire();
+		UserDetailsPasswordManagerBeanConfig.Manager manager = this.spring.getContext().getBean(UserDetailsPasswordManagerBeanConfig.Manager.class);
+		AuthenticationManager am = this.spring.getContext().getBean(AuthenticationConfiguration.class).getAuthenticationManager();
+		when(manager.loadUserByUsername("user")).thenReturn(User.withUserDetails(user).build(), User.withUserDetails(user).build());
+		when(manager.updatePassword(any(), any())).thenReturn(user);
+
+		am.authenticate(new UsernamePasswordAuthenticationToken("user", "password"));
+
+		verify(manager).updatePassword(eq(user), startsWith("{bcrypt}"));
+	}
+
+	@Configuration
+	@Import({AuthenticationConfiguration.class, ObjectPostProcessorConfiguration.class})
+	static class UserDetailsPasswordManagerBeanConfig {
+		Manager manager = mock(Manager.class);
+
+		@Bean
+		UserDetailsService userDetailsService() {
+			return this.manager;
+		}
+
+		interface Manager extends UserDetailsService, UserDetailsPasswordService {
+		}
+	}
+
 	//gh-3091
 	@Test
 	public void getAuthenticationWhenAuthenticationProviderBeanThenUsed() throws Exception {

+ 59 - 1
config/src/test/java/org/springframework/security/config/annotation/web/WebSecurityConfigurerAdapterTests.java

@@ -59,7 +59,9 @@ import java.util.List;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.ThrowableAssert.catchThrowable;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
@@ -175,6 +177,62 @@ public class WebSecurityConfigurerAdapterTests {
 		}
 	}
 
+	@Test
+	public void loadConfigWhenInMemoryConfigureProtectedThenPasswordUpgraded() throws Exception {
+		this.spring.register(InMemoryConfigureProtectedConfig.class).autowire();
+
+		this.mockMvc.perform(formLogin())
+				.andExpect(status().is3xxRedirection());
+
+		UserDetailsService uds = this.spring.getContext()
+				.getBean(UserDetailsService.class);
+		assertThat(uds.loadUserByUsername("user").getPassword()).startsWith("{bcrypt}");
+	}
+
+	@EnableWebSecurity
+	static class InMemoryConfigureProtectedConfig extends WebSecurityConfigurerAdapter {
+		@Override
+		protected void configure(AuthenticationManagerBuilder auth) throws Exception {
+			auth
+				.inMemoryAuthentication()
+					.withUser(PasswordEncodedUser.user());
+		}
+
+		@Override
+		@Bean
+		public UserDetailsService userDetailsServiceBean() throws Exception {
+			return super.userDetailsServiceBean();
+		}
+	}
+
+	@Test
+	public void loadConfigWhenInMemoryConfigureGlobalThenPasswordUpgraded() throws Exception {
+		this.spring.register(InMemoryConfigureGlobalConfig.class).autowire();
+
+		this.mockMvc.perform(formLogin())
+				.andExpect(status().is3xxRedirection());
+
+		UserDetailsService uds = this.spring.getContext()
+				.getBean(UserDetailsService.class);
+		assertThat(uds.loadUserByUsername("user").getPassword()).startsWith("{bcrypt}");
+	}
+
+	@EnableWebSecurity
+	static class InMemoryConfigureGlobalConfig extends WebSecurityConfigurerAdapter {
+		@Autowired
+		public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
+			auth
+				.inMemoryAuthentication()
+					.withUser(PasswordEncodedUser.user());
+		}
+
+		@Override
+		@Bean
+		public UserDetailsService userDetailsServiceBean() throws Exception {
+			return super.userDetailsServiceBean();
+		}
+	}
+
 	@Test
 	public void loadConfigWhenCustomContentNegotiationStrategyBeanThenOverridesDefault() throws Exception {
 		OverrideContentNegotiationStrategySharedObjectConfig.CONTENT_NEGOTIATION_STRATEGY_BEAN = mock(ContentNegotiationStrategy.class);

+ 8 - 9
config/src/test/java/org/springframework/security/config/annotation/web/reactive/EnableWebFluxSecurityTests.java

@@ -16,14 +16,6 @@
 
 package org.springframework.security.config.annotation.web.reactive;
 
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
-import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
-import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials;
-
-import java.nio.charset.StandardCharsets;
-import java.security.Principal;
-
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -69,9 +61,16 @@ import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.reactive.config.EnableWebFlux;
 import org.springframework.web.reactive.function.BodyInserters;
 import org.springframework.web.reactive.result.view.AbstractView;
-
 import reactor.core.publisher.Mono;
 
+import java.nio.charset.StandardCharsets;
+import java.security.Principal;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
+import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.Credentials.basicAuthenticationCredentials;
+import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
+
 /**
  * @author Rob Winch
  * @since 5.0