From 691bf2e11d7d4ae6317ba15105ef233e789444a7 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 27 Nov 2017 10:57:28 -0600 Subject: [PATCH] PasswordEncoder Bean for AuthenticationManagerBuilder Issue: gh-4873 --- .../AuthenticationConfiguration.java | 97 ++++++++++++++- ...alizeUserDetailsBeanManagerConfigurer.java | 1 + .../WebSecurityConfigurerAdapter.java | 110 ++++++++++++++++-- .../AuthenticationManagerBuilderTests.groovy | 52 +++++++++ .../dao/DaoAuthenticationProvider.java | 3 +- .../dao/DaoAuthenticationProviderTests.java | 3 +- 6 files changed, 248 insertions(+), 18 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java index 04e4f407a1..114ed470a2 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfiguration.java @@ -27,6 +27,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.framework.ProxyFactoryBean; import org.springframework.aop.target.LazyInitTargetSource; import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @@ -36,9 +37,15 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer; +import org.springframework.security.config.annotation.authentication.configurers.provisioning.JdbcUserDetailsManagerConfigurer; +import org.springframework.security.config.annotation.authentication.configurers.userdetails.DaoAuthenticationConfigurer; import org.springframework.security.config.annotation.configuration.ObjectPostProcessorConfiguration; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.util.Assert; /** @@ -67,8 +74,9 @@ public class AuthenticationConfiguration { @Bean public AuthenticationManagerBuilder authenticationManagerBuilder( - ObjectPostProcessor objectPostProcessor) { - return new AuthenticationManagerBuilder(objectPostProcessor); + ObjectPostProcessor objectPostProcessor, ApplicationContext context) { + LazyPasswordEncoder defaultPasswordEncoder = new LazyPasswordEncoder(context); + return new DefaultPasswordEncoderAuthenticationManagerBuilder(objectPostProcessor, defaultPasswordEncoder); } @Bean @@ -92,7 +100,7 @@ public class AuthenticationConfiguration { return this.authenticationManager; } AuthenticationManagerBuilder authBuilder = authenticationManagerBuilder( - this.objectPostProcessor); + this.objectPostProcessor, this.applicationContext); if (this.buildingAuthenticationManager.getAndSet(true)) { return new AuthenticationManagerDelegator(authBuilder); } @@ -210,4 +218,85 @@ public class AuthenticationConfiguration { return "AuthenticationManagerDelegator [delegate=" + this.delegate + "]"; } } -} \ No newline at end of file + + static class DefaultPasswordEncoderAuthenticationManagerBuilder extends AuthenticationManagerBuilder { + private PasswordEncoder defaultPasswordEncoder; + + /** + * Creates a new instance + * + * @param objectPostProcessor the {@link ObjectPostProcessor} instance to use. + */ + DefaultPasswordEncoderAuthenticationManagerBuilder( + ObjectPostProcessor objectPostProcessor, PasswordEncoder defaultPasswordEncoder) { + super(objectPostProcessor); + this.defaultPasswordEncoder = defaultPasswordEncoder; + } + + @Override + public InMemoryUserDetailsManagerConfigurer inMemoryAuthentication() + throws Exception { + return super.inMemoryAuthentication() + .passwordEncoder(this.defaultPasswordEncoder); + } + + @Override + public JdbcUserDetailsManagerConfigurer jdbcAuthentication() + throws Exception { + return super.jdbcAuthentication() + .passwordEncoder(this.defaultPasswordEncoder); + } + + @Override + public DaoAuthenticationConfigurer userDetailsService( + T userDetailsService) throws Exception { + return super.userDetailsService(userDetailsService) + .passwordEncoder(this.defaultPasswordEncoder); + } + } + + static class LazyPasswordEncoder implements PasswordEncoder { + private ApplicationContext applicationContext; + private PasswordEncoder passwordEncoder; + + LazyPasswordEncoder(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public String encode(CharSequence rawPassword) { + return getPasswordEncoder().encode(rawPassword); + } + + @Override + public boolean matches(CharSequence rawPassword, + String encodedPassword) { + return getPasswordEncoder().matches(rawPassword, encodedPassword); + } + + private PasswordEncoder getPasswordEncoder() { + if (this.passwordEncoder != null) { + return this.passwordEncoder; + } + PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class); + if (passwordEncoder == null) { + passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } + this.passwordEncoder = passwordEncoder; + return passwordEncoder; + } + + private T getBeanOrNull(Class type) { + try { + return this.applicationContext.getBean(type); + } catch(NoSuchBeanDefinitionException notFound) { + return null; + } + } + + @Override + public String toString() { + return getPasswordEncoder().toString(); + } + } +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java index f93430668d..54e9ac4d0c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java @@ -71,6 +71,7 @@ class InitializeUserDetailsBeanManagerConfigurer if (passwordEncoder != null) { provider.setPasswordEncoder(passwordEncoder); } + provider.afterPropertiesSet(); auth.authenticationProvider(provider); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java index 9ca593f87b..aa21667b9a 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java @@ -31,6 +31,7 @@ import org.springframework.aop.framework.Advised; import org.springframework.aop.target.LazyInitTargetSource; import org.springframework.beans.FatalBeanException; import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.core.annotation.Order; @@ -42,6 +43,9 @@ import org.springframework.security.authentication.DefaultAuthenticationEventPub import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer; +import org.springframework.security.config.annotation.authentication.configurers.provisioning.JdbcUserDetailsManagerConfigurer; +import org.springframework.security.config.annotation.authentication.configurers.userdetails.DaoAuthenticationConfigurer; import org.springframework.security.config.annotation.web.WebSecurityConfigurer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; @@ -53,6 +57,8 @@ import org.springframework.security.core.AuthenticationException; 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.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; import org.springframework.util.Assert; @@ -365,6 +371,19 @@ public abstract class WebSecurityConfigurerAdapter implements @Autowired public void setApplicationContext(ApplicationContext context) { this.context = context; + + ObjectPostProcessor objectPostProcessor = context.getBean(ObjectPostProcessor.class); + LazyPasswordEncoder passwordEncoder = new LazyPasswordEncoder(context); + + authenticationBuilder = new DefaultPasswordEncoderAuthenticationManagerBuilder(objectPostProcessor, passwordEncoder); + localConfigureAuthenticationBldr = new DefaultPasswordEncoderAuthenticationManagerBuilder(objectPostProcessor, passwordEncoder) { + @Override + public AuthenticationManagerBuilder eraseCredentials(boolean eraseCredentials) { + authenticationBuilder.eraseCredentials(eraseCredentials); + return super.eraseCredentials(eraseCredentials); + } + + }; } @Autowired(required = false) @@ -381,17 +400,6 @@ public abstract class WebSecurityConfigurerAdapter implements @Autowired public void setObjectPostProcessor(ObjectPostProcessor objectPostProcessor) { this.objectPostProcessor = objectPostProcessor; - - authenticationBuilder = new AuthenticationManagerBuilder(objectPostProcessor); - localConfigureAuthenticationBldr = new AuthenticationManagerBuilder( - objectPostProcessor) { - @Override - public AuthenticationManagerBuilder eraseCredentials(boolean eraseCredentials) { - authenticationBuilder.eraseCredentials(eraseCredentials); - return super.eraseCredentials(eraseCredentials); - } - - }; } @Autowired @@ -530,4 +538,84 @@ public abstract class WebSecurityConfigurerAdapter implements } } + static class DefaultPasswordEncoderAuthenticationManagerBuilder extends AuthenticationManagerBuilder { + private PasswordEncoder defaultPasswordEncoder; + + /** + * Creates a new instance + * + * @param objectPostProcessor the {@link ObjectPostProcessor} instance to use. + */ + DefaultPasswordEncoderAuthenticationManagerBuilder( + ObjectPostProcessor objectPostProcessor, PasswordEncoder defaultPasswordEncoder) { + super(objectPostProcessor); + this.defaultPasswordEncoder = defaultPasswordEncoder; + } + + @Override + public InMemoryUserDetailsManagerConfigurer inMemoryAuthentication() + throws Exception { + return super.inMemoryAuthentication() + .passwordEncoder(this.defaultPasswordEncoder); + } + + @Override + public JdbcUserDetailsManagerConfigurer jdbcAuthentication() + throws Exception { + return super.jdbcAuthentication() + .passwordEncoder(this.defaultPasswordEncoder); + } + + @Override + public DaoAuthenticationConfigurer userDetailsService( + T userDetailsService) throws Exception { + return super.userDetailsService(userDetailsService) + .passwordEncoder(this.defaultPasswordEncoder); + } + } + + static class LazyPasswordEncoder implements PasswordEncoder { + private ApplicationContext applicationContext; + private PasswordEncoder passwordEncoder; + + LazyPasswordEncoder(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public String encode(CharSequence rawPassword) { + return getPasswordEncoder().encode(rawPassword); + } + + @Override + public boolean matches(CharSequence rawPassword, + String encodedPassword) { + return getPasswordEncoder().matches(rawPassword, encodedPassword); + } + + private PasswordEncoder getPasswordEncoder() { + if (this.passwordEncoder != null) { + return this.passwordEncoder; + } + PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class); + if (passwordEncoder == null) { + passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } + this.passwordEncoder = passwordEncoder; + return passwordEncoder; + } + + private T getBeanOrNull(Class type) { + try { + return this.applicationContext.getBean(type); + } catch(NoSuchBeanDefinitionException notFound) { + return null; + } + } + + @Override + public String toString() { + return getPasswordEncoder().toString(); + } + } } diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/authentication/AuthenticationManagerBuilderTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/authentication/AuthenticationManagerBuilderTests.groovy index b50774b10e..6a33a133a7 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/authentication/AuthenticationManagerBuilderTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/authentication/AuthenticationManagerBuilderTests.groovy @@ -15,6 +15,7 @@ */ package org.springframework.security.config.annotation.authentication +import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration @@ -37,6 +38,8 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur import org.springframework.security.core.Authentication import org.springframework.security.core.userdetails.PasswordEncodedUser import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.crypto.password.NoOpPasswordEncoder +import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.provisioning.InMemoryUserDetailsManager; /** @@ -71,6 +74,55 @@ class AuthenticationManagerBuilderTests extends BaseSpringSpec { am.eventPublisher == aep } + def "PasswordEncoder bean is used for Global"() { + setup: + loadConfig(PasswordEncoderGlobalConfig) + when: + Authentication auth = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken("user","password")) + then: + auth.name == "user" + auth.authorities*.authority == ['ROLE_USER'] + } + + @EnableWebSecurity + static class PasswordEncoderGlobalConfig extends WebSecurityConfigurerAdapter { + @Autowired + void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER") + } + + @Bean + PasswordEncoder passwordEncoder() { + return NoOpPasswordEncoder.getInstance(); + } + } + + def "PasswordEncoder bean is used for protected"() { + setup: + loadConfig(PasswordEncoderConfig) + when: + Authentication auth = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken("user","password")) + then: + auth.name == "user" + auth.authorities*.authority == ['ROLE_USER'] + } + + @EnableWebSecurity + static class PasswordEncoderConfig extends WebSecurityConfigurerAdapter { + void configure(AuthenticationManagerBuilder auth) throws Exception { + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER") + } + + @Bean + PasswordEncoder passwordEncoder() { + return NoOpPasswordEncoder.getInstance(); + } + } + def "authentication-manager support multiple DaoAuthenticationProvider's"() { setup: loadConfig(MultiAuthenticationProvidersConfig) diff --git a/core/src/main/java/org/springframework/security/authentication/dao/DaoAuthenticationProvider.java b/core/src/main/java/org/springframework/security/authentication/dao/DaoAuthenticationProvider.java index 0dbd7d0959..3cdc43622f 100644 --- a/core/src/main/java/org/springframework/security/authentication/dao/DaoAuthenticationProvider.java +++ b/core/src/main/java/org/springframework/security/authentication/dao/DaoAuthenticationProvider.java @@ -94,6 +94,7 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication protected void doAfterPropertiesSet() throws Exception { Assert.notNull(this.userDetailsService, "A UserDetailsService must be set"); + this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD); } protected final UserDetails retrieveUser(String username, @@ -138,8 +139,6 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication */ public void setPasswordEncoder(PasswordEncoder passwordEncoder) { Assert.notNull(passwordEncoder, "passwordEncoder cannot be null"); - - this.userNotFoundEncodedPassword = passwordEncoder.encode(USER_NOT_FOUND_PASSWORD); this.passwordEncoder = passwordEncoder; } diff --git a/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java b/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java index 68b254f036..d09ae5bba7 100644 --- a/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java +++ b/core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java @@ -483,7 +483,7 @@ public class DaoAuthenticationProviderTests { // SEC-2056 @Test - public void testUserNotFoundEncodesPassword() { + public void testUserNotFoundEncodesPassword() throws Exception { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( "missing", "koala"); PasswordEncoder encoder = mock(PasswordEncoder.class); @@ -492,6 +492,7 @@ public class DaoAuthenticationProviderTests { provider.setHideUserNotFoundExceptions(false); provider.setPasswordEncoder(encoder); provider.setUserDetailsService(new MockAuthenticationDaoUserrod()); + provider.afterPropertiesSet(); try { provider.authenticate(token); fail("Expected Exception");