Reputation: 1367
I'm fighting a "bug" similar to one Apple had recently in OS X :) An application authenticates users treating their password field not only as a bcrypt hash but also as a plaintext, so it allows special utility accounts to log in with an empty password.
There are a bunch of user records in the database and almost all of them has their password hashed with bcrypt. There are however a few special utility accounts with password hash field deliberately left empty (to make BcryptPasswordEncoder#matches
always reject login attempts for them).
Placing breakpoints all over ProviderManager
I can see multiple authentication providers initialized by spring:
DaoAuthenticationProvider
with bcrypt encoderAnonymousAuthenticationProvider
, which nobody configured, but at least I can guess it came from permitAll()
or something alike.DaoAuthenticationProvider
with PlaintextPasswordEncoder
which spoils all funWe have another project where we don't use Spring Boot and with almost identical configuration it works as expected (passwords are never treated as plaintext, only as bcrypt hash). So my guess is: this "problem" has something to do with Spring Boot "configuration by convention" and I can't find how to override its behavior.
In this project I use the following configuration:
@Configuration
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
AuthenticationProvider authenticationProvider;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.userDetailsService(userDetailsService)
.authorizeRequests()
.antMatchers("/js/**", "/css/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login").permitAll()
.loginProcessingUrl("/j_spring_security_check").permitAll()
.successHandler(new SuccessHandler())
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout")).permitAll()
.logoutSuccessUrl("/login");
http.csrf().disable();
http.headers().frameOptions().sameOrigin();
}
@Autowired
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
auth.authenticationProvider(authenticationProvider);
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
final DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(15);
}
}
Edit: If I get it correct, there's a way to configure global and local AuthenticationManagerBuilder
s:
// Inject and configure global:
/*
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
*/
// Override method and configure the local one:
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
Doing that, I have now two instances of builder: one - local - has only the correct manager: bcrypt, the other one - global - has other 2 providers: anonymous and plaintext. Authentication behavior persists, application still uses both and lets users login with plaintext passwords. Uncommenting configureGlobal
doesn't help too, in that case, global manager contains all three providers.
Upvotes: 1
Views: 1863
Reputation: 1367
It turned out that protected void configure(HttpSecurity http)
was triggering the second AuthenticationManagerBuilder
creation. So I provided my AuthenticationProvider
bean and added it to httpsecurity configuration. Everything seems to work as expected now. It might not be the correct solution though.
New configuration (works for me):
@Configuration
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
AuthenticationProvider authenticationProvider;
@Autowired
PasswordEncoder passwordEncoder;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authenticationProvider(authenticationProvider) // <== Important
//.anonymous().disable() // <== This part is OK. If enabled, adds an anonymousprovider; if disabled, it is impossible to login due to "unauthenticated<->authenticate" endless loop.
.httpBasic().disable()
.rememberMe().disable()
.authorizeRequests()
.antMatchers("/js/**", "/css/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login").permitAll()
.loginProcessingUrl("/j_spring_security_check").permitAll()
.successHandler(new SuccessHandler())
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout")).permitAll()
.logoutSuccessUrl("/login");
}
@Bean
public AuthenticationProvider authenticationProvider() {
final DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(15);
}
}
Upvotes: 0
Reputation: 21720
The configuration is explicitly providing the userDetailsService in multiple places without providing the PasswordEncoder
. The easiest solution is to expose the UserDetaisService
and PasswordEncoder
as a Bean and delete all explicit configuration. This works because if there is no explicit configuration, Spring Security will discover the Beans and create authentication from them.
@Configuration
@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http // Don't forget to remove userDetailsService
.authorizeRequests()
.antMatchers("/js/**", "/css/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login").permitAll()
.loginProcessingUrl("/j_spring_security_check").permitAll()
.successHandler(new SuccessHandler())
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout")).permitAll()
.logoutSuccessUrl("/login");
http.csrf().disable();
http.headers().frameOptions().sameOrigin();
}
// UserDetailsService appears to be a Bean somewhere else, but make sure you have one defined as a Bean
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(15);
}
}
The reason it is failing is because there is explicit configuration to use the UserDetailsService twice:
@Autowired
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
// below Configures UserDetailsService with no PasswordEncoder
auth.userDetailsService(userDetailsService);
// configures the same UserDetailsService (it was used to create the authenticationProvider) with a PasswordEncoder (it was provided to the authenticationProvider)
auth.authenticationProvider(authenticationProvider);
}
If you want explicit configuration, you could use the following
@Override
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
and delete the authenticationProvider
bean along with the @Autowired AuthenticationProvider
. Alternatively, you could just use the AuthenticationProvider
, but not both.
Generally, the explicit configuration of AuthenticationManagerBuilder is only needed when you have multiple WebSecurityConfigurerAdapter
with different authentication mechanisms. If you do not have the need for this, I recommend just exposing the UserDetailsService
and (optionally) the PasswordEncoder
as a Bean.
Note that if you expose a AuthenticationProvider
as a Bean it is used over a UserDetailsService
. Similarly, if you expose an AuthenticationManager
as a Bean it is used over AuthenticationProvider
. Finally, if you explicitly provide AuthenticationManagerBuilder
configuration, it is used over any Bean definitions.
Upvotes: 4