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 c82469ee67..39bbba45c4 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 @@ -68,6 +68,11 @@ public class AuthenticationConfiguration { return new EnableGlobalAuthenticationAutowiredConfigurer(context); } + @Bean + public static InitializeUserDetailsBeanManagerConfigurer initializeUserDetailsBeanManagerConfigurer(ApplicationContext context) { + return new InitializeUserDetailsBeanManagerConfigurer(context); + } + public AuthenticationManager getAuthenticationManager() throws Exception { if (authenticationManagerInitialized) { return authenticationManager; 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 new file mode 100644 index 0000000000..8bdaf03143 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/authentication/configuration/InitializeUserDetailsBeanManagerConfigurer.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * 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.config.annotation.authentication.configuration; + +import org.springframework.context.ApplicationContext; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.authentication.configurers.GlobalAuthenticationConfigurerAdapter; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * Lazily initializes the global authentication with a + * {@link UserDetailsService} if it is not yet configured and there is only a + * single Bean of that type. Optionally, if a {@link PasswordEncoder} is defined + * will wire this up too. + * + * @author Rob Winch + * @since 4.1 + */ +@Order(Ordered.LOWEST_PRECEDENCE - 5000) +class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationConfigurerAdapter { + + private final ApplicationContext context; + + /** + * @param context + */ + public InitializeUserDetailsBeanManagerConfigurer(ApplicationContext context) { + this.context = context; + } + + @Override + public void init(AuthenticationManagerBuilder auth) throws Exception { + auth.apply(new InitializeUserDetailsManagerConfigurer()); + } + + class InitializeUserDetailsManagerConfigurer extends GlobalAuthenticationConfigurerAdapter { + @Override + public void configure(AuthenticationManagerBuilder auth) throws Exception { + if (auth.isConfigured()) { + return; + } + UserDetailsService userDetailsService = getBeanOrNull(UserDetailsService.class); + if(userDetailsService == null) { + return; + } + + PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class); + + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setUserDetailsService(userDetailsService); + if(passwordEncoder != null) { + provider.setPasswordEncoder(passwordEncoder); + } + + auth + .authenticationProvider(provider); + } + + /** + * @return + */ + private T getBeanOrNull(Class type) { + String[] userDetailsBeanNames = context.getBeanNamesForType(type); + if(userDetailsBeanNames.length != 1) { + return null; + } + + return context.getBean(userDetailsBeanNames[0], type); + } + } +} \ No newline at end of file diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.groovy index a11da5375b..cab0599856 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/authentication/configuration/AuthenticationConfigurationTests.groovy @@ -16,13 +16,7 @@ package org.springframework.security.config.annotation.authentication.configuration; import org.springframework.aop.framework.ProxyFactoryBean -import org.springframework.beans.BeansException import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.config.BeanPostProcessor -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory -import org.springframework.beans.factory.support.BeanDefinitionRegistry -import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor -import org.springframework.context.ApplicationContext import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Import @@ -41,11 +35,13 @@ import org.springframework.security.config.annotation.configuration.ObjectPostPr import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter -import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity import org.springframework.security.core.AuthenticationException import org.springframework.security.core.authority.AuthorityUtils import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.userdetails.User +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 class AuthenticationConfigurationTests extends BaseSpringSpec { @@ -366,4 +362,65 @@ class AuthenticationConfigurationTests extends BaseSpringSpec { static class BootGlobalAuthenticationConfigurerAdapter extends GlobalAuthenticationConfigurerAdapter { } } + + def 'SEC-2868: Allow Configure UserDetailsService'() { + setup: + UserDetailsService uds = Mock() + UserDetailsServiceBeanConfig.UDS = uds + loadConfig(UserDetailsServiceBeanConfig) + AuthenticationManager am = context.getBean(AuthenticationConfiguration).getAuthenticationManager() + when: + am.authenticate(new UsernamePasswordAuthenticationToken("user", "password")) + then: + 1 * uds.loadUserByUsername("user") >> new User("user","password",AuthorityUtils.createAuthorityList("ROLE_USER")) + when: + am.authenticate(new UsernamePasswordAuthenticationToken("user", "invalid")) + then: + 1 * uds.loadUserByUsername("user") >> new User("user","password",AuthorityUtils.createAuthorityList("ROLE_USER")) + thrown(AuthenticationException.class) + } + + @Configuration + @Import([AuthenticationConfiguration, ObjectPostProcessorConfiguration]) + static class UserDetailsServiceBeanConfig { + static UserDetailsService UDS + + @Bean + UserDetailsService userDetailsService() { + UDS + } + } + + def 'SEC-2868: Allow Configure UserDetailsService with PasswordEncoder'() { + setup: + UserDetailsService uds = Mock() + UserDetailsServiceBeanWithPasswordEncoderConfig.UDS = uds + loadConfig(UserDetailsServiceBeanWithPasswordEncoderConfig) + AuthenticationManager am = context.getBean(AuthenticationConfiguration).getAuthenticationManager() + when: + am.authenticate(new UsernamePasswordAuthenticationToken("user", "password")) + then: + 1 * uds.loadUserByUsername("user") >> new User("user",'$2a$10$FBAKClV1zBIOOC9XMXf3AO8RoGXYVYsfvUdoLxGkd/BnXEn4tqT3u',AuthorityUtils.createAuthorityList("ROLE_USER")) + when: + am.authenticate(new UsernamePasswordAuthenticationToken("user", "invalid")) + then: + 1 * uds.loadUserByUsername("user") >> new User("user",'$2a$10$FBAKClV1zBIOOC9XMXf3AO8RoGXYVYsfvUdoLxGkd/BnXEn4tqT3u',AuthorityUtils.createAuthorityList("ROLE_USER")) + thrown(AuthenticationException.class) + } + + @Configuration + @Import([AuthenticationConfiguration, ObjectPostProcessorConfiguration]) + static class UserDetailsServiceBeanWithPasswordEncoderConfig { + static UserDetailsService UDS + + @Bean + UserDetailsService userDetailsService() { + UDS + } + + @Bean + PasswordEncoder passwordEncoder() { + new BCryptPasswordEncoder() + } + } } \ No newline at end of file diff --git a/docs/manual/src/docs/asciidoc/index.adoc b/docs/manual/src/docs/asciidoc/index.adoc index bc971e94a0..95ce355142 100644 --- a/docs/manual/src/docs/asciidoc/index.adoc +++ b/docs/manual/src/docs/asciidoc/index.adoc @@ -374,6 +374,7 @@ This will give you access to the entire project history (including all releases ** <> * <> * <> +* <> === What's new in Spring Security 4.0 @@ -912,6 +913,33 @@ cn: admin uniqueMember: uid=admin,ou=people,dc=springframework,dc=org ---- +[[jc-authentication-userdetailsservice]] +==== UserDetailsService + +You can define custom authentication by exposing a custom `UserDetailsService` as a bean. +For example, the following will customize authentication assuming that `SpringDataUserDetailsService` implements `UserDetailsService`: + +[source,java] +---- +@Bean +public SpringDataUserDetailsService springDataUserDetailsService() { + return new SpringDataUserDetailsService(); +} +---- + +You can also customize how passwords are encoded by exposing a `PasswordEncoder` as a bean. +For example, if you use bcrypt you can add a bean definition as shown below: + +[source,java] +---- +@Bean +public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); +} +---- + +==== LDAP Authentication + === Multiple HttpSecurity We can configure multiple HttpSecurity instances just as we can have multiple `` blocks. The key is to extend the `WebSecurityConfigurationAdapter` multiple times. For example, the following is an example of having a different configuration for URL's that start with `/api/`.