spring-security 6.0 migration - How to configure AuthenticationManager

2k Views Asked by At

I am migrating a project from spring boot 2.8 to spring boot 3. We used the code below to configure AuthentiationManagerBuilder in previous version. We cannot use configure method anymore because WebSecurityConfigurerAdapter has been removed in spring security 6.

  @SuppressWarnings("deprecation")
  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    if (!this.dmeAppSettings.isDevMode()) { // production
      auth.authenticationEventPublisher(authEventPub)
          .userDetailsService(userDetailsService())
          .passwordEncoder(pwdEncoder);
    } else {
      log.info("Configured to use in memory user accounts for authentication");
      auth.authenticationEventPublisher(authEventPub)
          .userDetailsService(userDetailsService())
          .and()
          .inMemoryAuthentication()
          .passwordEncoder(
              org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance())
          .withUser("user")
          .password("password")
          .roles("USER")
          .and()
          .passwordEncoder(
              org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance())
          .withUser("admin")
          .password("password")
          .roles(ADMIN_ONLY_ROLES)
          .and()
          .passwordEncoder(
              org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance())
          .withUser("sysadmin")
          .password("password")
          .roles(SYS_ADMIN_ROLES)
          .and()
          .passwordEncoder(
              org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance())
          .withUser("fsadmin")
          .password("password")
          .roles(FS_ADMIN_ROLES)
          .and()
          .passwordEncoder(
              org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance())
          .withUser("centcomadmin")
          .password("password")
          .roles(CENTCOM_ADMIN_ROLES);
    }
  }

  @Bean
  public UserDetailsService userDetailsService() {
    if (this.dmeAppSettings.isDevMode()) { // Dev mode.
      return new UserDetailsService() {
        @Override
        public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
          List<GrantedAuthority> auths = new ArrayList<>();
          if (userId.equalsIgnoreCase("ADMIN")) {
            for (String admin : ADMIN_ONLY_ROLES) {
              auths.add(new SimpleGrantedAuthority("ROLE_" + admin));
            }
          } else if (userId.equalsIgnoreCase("SYSADMIN")) {
            for (String admin : SYS_ADMIN_ROLES) {
              auths.add(new SimpleGrantedAuthority("ROLE_" + admin));
            }
          } else if (userId.equalsIgnoreCase("FSSADMIN")) {
            for (String admin : FS_ADMIN_ROLES) {
              auths.add(new SimpleGrantedAuthority("ROLE_" + admin));
            }
          } else if (userId.equalsIgnoreCase("CENTCOMADMIN")) {
            for (String admin : CENTCOM_ADMIN_ROLES) {
              auths.add(new SimpleGrantedAuthority("ROLE_" + admin));
            }
          }
          User result = new User(userId, "password", true, true, true, true, auths);
          return result;
        }
      };
    } else { // Production mode.
      return new DmeappUserService(dmeAppSettings, userServiceDb);
    }
  }

Basically, I want to configure the following settings via AuthenticationManagerBuilder:

  • Set custom authentication event publisher.
  • Use in memory authentication in dev mode.
  • Use database service in production mode.

I created AuthenticationManager Bean but got an exception. Is this the right way to configure AuthenticationManager?

  @Bean
  public AuthenticationManager authenticationManager(
      AuthenticationManagerBuilder auth)
      throws Exception {
    if (!this.dmeAppSettings.isDevMode()) { // production
      auth.authenticationEventPublisher(authEventPub)
          .userDetailsService(userDetailsService())
          .passwordEncoder(pwdEncoder);
    } else {
      log.info("Configured to use in memory user accounts for authentication");
      auth.authenticationEventPublisher(authEventPub)
          .userDetailsService(userDetailsService())
          .and()
          .inMemoryAuthentication()
          .passwordEncoder(
              org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance())
          .withUser("user").password("password").roles("USER")
          .and()
          .withUser("admin").password("password").roles(ADMIN_ONLY_ROLES)
          .and()
          .withUser("sysadmin").password("password").roles(SYS_ADMIN_ROLES)
          .and()
          .withUser("fsadmin").password("password").roles(FS_ADMIN_ROLES)
          .and()
          .withUser("centcomadmin").password("password").roles(CENTCOM_ADMIN_ROLES);
    }
    return auth.build();
  }

Exception stack trace:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'authenticationManager' defined in class path resource [com/missionessential/dmeapp/config/DmeappSecurityConfig.class]: Failed to instantiate [org.springframework.security.authentication.AuthenticationManager]: Factory method 'authenticationManager' threw exception with message: Cannot apply org.springframework.security.config.annotation.authentication.configurers.userdetails.DaoAuthenticationConfigurer@520a8ea1 to already built object
        at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:652)
        at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:640)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1324)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1161)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:561)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:521)
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326)
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324)
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:961)
        at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:915)
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:584)
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146)
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:730)
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:432)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:308)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1302)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1291)
        at com.missionessential.dmeapp.DmeappApplication.main(DmeappApplication.java:63)
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.authentication.AuthenticationManager]: Factory method 'authenticationManager' threw exception with message: Cannot apply org.springframework.security.config.annotation.authentication.configurers.userdetails.DaoAuthenticationConfigurer@520a8ea1 to already built object
        at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:171)
        at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:648)
        ... 19 common frames omitted
Caused by: java.lang.IllegalStateException: Cannot apply org.springframework.security.config.annotation.authentication.configurers.userdetails.DaoAuthenticationConfigurer@520a8ea1 to already built object
        at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.add(AbstractConfiguredSecurityBuilder.java:182)
        at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.apply(AbstractConfiguredSecurityBuilder.java:125)
        at org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder.apply(AuthenticationManagerBuilder.java:280)
        at org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder.userDetailsService(AuthenticationManagerBuilder.java:182)
        at org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration$DefaultPasswordEncoderAuthenticationManagerBuilder.userDetailsService(AuthenticationConfiguration.java:298)
        at com.missionessential.dmeapp.config.DmeappSecurityConfig.authenticationManager(DmeappSecurityConfig.java:230)
        at com.missionessential.dmeapp.config.DmeappSecurityConfig$$SpringCGLIB$$0.CGLIB$authenticationManager$0(<generated>)
        at com.missionessential.dmeapp.config.DmeappSecurityConfig$$SpringCGLIB$$2.invoke(<generated>)
        at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:257)
        at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:331)
        at com.missionessential.dmeapp.config.DmeappSecurityConfig$$SpringCGLIB$$0.authenticationManager(<generated>)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:568)
        at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:139)
        ... 20 common frames omitted

2

There are 2 best solutions below

0
On
Hope below idea can help 

Consider Student have email and password 

- create a repository StudentRepository.java
  
  public interface StudentRepository extends JpaRepository<Student, Long> {
    Optional<Student> findByEmail(String email);
  }



- Create below custom StudentAuthenticationProvider.java
  
@Component
@RequiredArgsConstructor
public class StudentAuthenticationProvider implements AuthenticationProvider, UserDetailsService {

    private final StudentRepository studentRepository;
    
    /**
     * <p> The authenticate method to authenticate the request. We will get the username from the Authentication object and will
     * use the custom @userDetailsService service to load the given user.</p>
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        final String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
        if (Utils.isEmpty(username)) {
            throw new BadCredentialsException("invalid login details");
        }
        // get user details using Spring security user details service
        UserDetails user = null;
        try {
            user = loadUserByUsername(username);

        } catch (UsernameNotFoundException exception) {
            throw new BadCredentialsException("invalid login details");
        }
        return createSuccessfulAuthentication(authentication, user);
    }

    private Authentication createSuccessfulAuthentication(final Authentication authentication, final UserDetails user) {
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user.getUsername(), authentication.getCredentials(), user.getAuthorities());
        token.setDetails(authentication.getDetails());
        return token;
    }

    @Override
    public boolean supports(Class < ? > authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws 
    UsernameNotFoundException {
        return studentRepository.findByEmail(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));
    }
} 

- Update SecurityFilterChain configuration SecurityConfig.java
  
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final StudentAuthenticationProvider studentAuthenticationProvider;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(Customizer.withDefaults())
                .authorizeHttpRequests(req ->
                        req.requestMatchers("/appControllers/auth/**" , "/dist/**")
                                .permitAll()
                                .anyRequest()
                                .authenticated()
                )
                .authenticationProvider(studentAuthenticationProvider)
                .httpBasic(Customizer.withDefaults())
                .formLogin(Customizer.withDefaults());
        return http.build();
    }

}
0
On

try replacing method signature - injecting AuthenticationConfiguration instead AuthenticationManagerBuilder:

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

this will relate to your custom beans implicitly (userDetailsService, passwordEncoder...)

if there is a reason you'll explicitly need to get AuthenticationManagerBuilder do it using the following (not recommended, but will also work)

  @Bean
  public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        // retrieve builder from httpSecurity
        AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);

        authenticationManagerBuilder
                .userDetailsService(userDetailsService())
                .passwordEncoder(passwordEncoder());
        return authenticationManagerBuilder.build();
  }

one another subject - differentiate implementation for UserDetailsService per active profile (spring.profiles.active) can be achieved by @Profile at @Bean or @Configuration level (cleaner, especially if you have several beans)

    @Bean
    @Profile({"dev"})
    public UserDetailsService userDetailsServiceDev() {
        // see @EnableWebSecurity documentation
        return new InMemoryUserDetailsManager(...);
    }

    @Bean
    @Profile({"!dev"})
    public UserDetailsService userDetailsService() {
        // your custom implementation
    }