Search code examples
javaspringspring-bootspring-security

Spring Security: Multiple Basic Auth with different user stores without WebSecurityConfigurerAdapter


I am currently refactoring the security configuration removing WebSecurityConfigurerAdapter and am currently stuck on a config using two Basic Auth configurations with different user stores on different paths.

Current configuration looks like this and works fine:

@EnableWebSecurity
public class SecurityConfig {

    @Order(1)
    @Configuration
    public static class BasicSpecialAuth extends WebSecurityConfigurerAdapter {

        // some code

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            // some code
 auth.inMemoryAuthentication().withUser(specialUser.getId()).password(passwordEncoder().encode(specialUser.getPassword())).roles("SPECIALROLE");
        }


        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.httpBasic()
                    .and()
                    .antMatcher("/very-special-path/**")
                    //. more code
                    .authorizeRequests(r -> r
                            .anyRequest().authenticated());
        }
    }

    @Order(2)
    @Configuration
    public static class BasicAppAuth extends WebSecurityConfigurerAdapter {

        // some code

        @Bean
        public CustomUserDetailsService customUserDetailsService() {
            return new CustomUserDetailsService(userRepository);
        }

        @Override
        protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(customUserDetailsService())
                    .passwordEncoder(encoder());
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.httpBasic()
                    .and()
                    //. more code
                    .authorizeRequests(auth -> auth
                            .anyRequest().authenticated());
        }
    }
}

As can be seen, /very-special-path uses InMemoryAuthentication set up at start by configuration. All other paths should be authenticated using users from local database. Due to possible duplicates on usernames I am not able to use the database for /very-special-path users too. Requirement is to have these separated.

Following documentation it was quite simple to change this on our apps providing Basic Auth and JWT Auth on different path. But with both using Basic Auth and different user stores, I have no idea how to set up configuration properly.

Any help would be appreciated.

Edit, the current config:

@Configuration
public class SecurityConfig {

    // some code

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService customUserDetailsService() {
        return new CustomUserDetailsService(userRepository);
    }

    @Bean
    public InMemoryUserDetailsManager inMemoryUserDetailsService() {
        // more code

        UserDetails healthUser = User.withUsername(specialUser.getId())
                .password(passwordEncoder().encode(specialUser.getPassword()))
                .roles("SPECIALROLE")
                .build();
        return new InMemoryUserDetailsManager(healthUser);
    }

    @Bean
    @Order(1)
    public SecurityFilterChain specialFilterChain(HttpSecurity http) throws Exception {
        http.httpBasic()
                .and()
                .antMatcher("/very-special-path/**")
                .authorizeRequests(auth -> auth
                        .anyRequest().authenticated());
        return http.build();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.httpBasic()
                .and()
                .authorizeRequests(auth -> auth
                        .anyRequest().authenticated());
        return http.build();
    }
}

The app starts without any Warning or Error.

Both chains are mentioned in the log:

o.s.s.web.DefaultSecurityFilterChain : Will secure any request with ..

o.s.s.web.DefaultSecurityFilterChain : Will secure Ant [pattern='/very-special-path/**'] with ..

But authentication does not work. Checked for different endpoints and with different users. Every request gets an 401. This config misses the assignment of the UserDetails to the specific filter chain. Is there a way to do so?


Solution

  • Based on the extra information you provided, there are only a couple of tweaks needed to get your configuration working. Take a look at this blog post that details common patterns for replacing WebSecurityConfigurerAdapter with the component-based approach, specifically the local AuthenticationManager.

    For the special/health user, you can manually construct a local authentication manager so as not to collide with the global one declared as an @Bean. Here's a full example:

    @EnableWebSecurity
    public class SecurityConfig {
    
        // ...
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Bean
        public UserDetailsService customUserDetailsService() {
            return new CustomUserDetailsService(userRepository);
        }
    
        @Bean
        @Order(1)
        public SecurityFilterChain specialFilterChain(HttpSecurity http, PasswordEncoder passwordEncoder) throws Exception {
            http
                .mvcMatcher("/very-special-path/**")
                .authorizeRequests((authorize) -> authorize
                    .anyRequest().authenticated()
                )
                .httpBasic(Customizer.withDefaults())
                .authenticationManager(specialAuthenticationManager(passwordEncoder));
    
            return http.build();
        }
    
        private AuthenticationManager specialAuthenticationManager(PasswordEncoder passwordEncoder) {
            UserDetailsService userDetailsService = specialUserDetailsService(passwordEncoder);
            DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
            authenticationProvider.setPasswordEncoder(passwordEncoder);
            authenticationProvider.setUserDetailsService(userDetailsService);
    
            return new ProviderManager(authenticationProvider);
        }
    
        private UserDetailsService specialUserDetailsService(PasswordEncoder passwordEncoder) {
            UserDetails specialUser = User.withUsername("specialuser")
                .password(passwordEncoder.encode("specialpassword"))
                .roles("SPECIALROLE")
                .build();
    
            return new InMemoryUserDetailsManager(specialUser);
        }
    
        @Bean
        @Order(2)
        public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            http
                .authorizeRequests((authorize) -> authorize
                    .anyRequest().authenticated()
                )
                .httpBasic(Customizer.withDefaults());
    
            return http.build();
        }
    
    }