Mikhail Antonov
Mikhail Antonov

Reputation: 1367

Spring Boot initializes more DaoAuthenticationProviders than expected

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:

We 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 AuthenticationManagerBuilders:

// 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

Answers (2)

Mikhail Antonov
Mikhail Antonov

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

Rob Winch
Rob Winch

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

Related Questions