Search code examples
javaspring-bootspring-securityjwtbasic-authentication

AuthenticationManager.authenticate() throws java.lang.StackOverflowError: null when just creating another class that implements AuthenticationProvider


In my project, I have two different authentication paths: the first one for normal user login and the second for employer login. So I begin by creating an authentication filter for the user using its AuthenticationProvider, and it works fine. For the second authentication, I create its authentication filter, and when I create another authentication provider for the employer, that is the problem. I get a java.lang.StackOverflowError: null when I try to authenticate the user's username and password using an instance of AuthenticationManager in authenticationmanager.authenticate(userPassAuthToken)

I didn't understand what happened when I created another authentication provider or why the authentication manager threw this exception. I think that the authentication manager is confused about which provider to use.

At first, I create the first authentication for a normal user with its filter and authentication provider as shown in the following code:

  1. User Filter class
@Component
public class UserPassAuthFilter extends OncePerRequestFilter {

    @Autowired
    @Lazy
    private AuthenticationManager authManager;
    @Autowired
    private AuthPath authPath;
    @Autowired
    AuthFilterUtil authFilterUtil;
    @Autowired
    private UserMapper userMapper;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        char[] username=request.getHeader("username").toCharArray();
        char[] password=request.getHeader("password").toCharArray();

        UserPassAuthToken authentication= new UserPassAuthToken(String.valueOf(username),String.valueOf(password));
        UserPassAuthToken currentAuth =(UserPassAuthToken)authManager.authenticate(authentication);
        if (!currentAuth.isAuthenticated()){
            authFilterUtil.onFailureAuth(response,request,null);
        }
        else {
            SecurityContextHolder.getContext().setAuthentication(currentAuth);
            if(request.getServletPath().equals(authPath.getUserPasswordFilter_pathShouldDo().get(0))) {
                UserDto responseBody = userMapper.mapUserToDto(currentAuth.getUsersDetails().getUsers());
                responseBody.setToken(authFilterUtil.prepareTokenToAuthResponse(
                        currentAuth.getAuthorities(),
                        currentAuth.getName().toCharArray()
                ));
                authFilterUtil.onSuccessfulAuth(response, responseBody);
            }
            filterChain.doFilter(request,response);
        }
    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {

        return !authPath.getUserPasswordFilter_pathShouldDo().contains(request.getServletPath());
    }
}

  1. user Authentication token class
public class UserPassAuthToken extends UsernamePasswordAuthenticationToken {
    @Getter
    @Setter
    private UsersDetails usersDetails;

    public UserPassAuthToken(Object principal, Object credentials) {
        super(principal, credentials);
    }

    public UserPassAuthToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(principal, credentials, authorities);
    }
}

  1. user authentication provider class
@Component
public class UserPassAuthProvider implements AuthenticationProvider {

    @Autowired
    UserService userDetailsService;
    @Autowired
    PasswordEncoder passwordEncoder;

    @Override
    public UserPassAuthToken authenticate(Authentication authentication) throws AuthenticationException {
        String username =authentication.getName();
        String password=(String)authentication.getCredentials();

        UserPassAuthToken userPassAuthToken;
        UsersDetails   user=userDetailsService.loadUserByUsername(username);
        if (user !=null && password != null && passwordEncoder.matches(password,user.getPassword())){
            userPassAuthToken=new UserPassAuthToken(username,user.getPassword(),user.getAuthorities());
            userPassAuthToken.setUsersDetails(user);
        }else {
            userPassAuthToken = new UserPassAuthToken(username, password);
            userPassAuthToken.setAuthenticated(false);
        }
        userPassAuthToken = new UserPassAuthToken(username, password);
        userPassAuthToken.setAuthenticated(false);
        return userPassAuthToken;
    }

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

  1. employer authentication provider class
@Component
public class EmployerAuthProvider implements AuthenticationProvider {

    @Autowired
    EmployerService employerService;
    @Autowired
    PasswordEncoder passwordEncoder;

    @Override
    public EmployerPassAuthToken authenticate(Authentication authentication) throws AuthenticationException {
        char[]username =authentication.getName().toCharArray();
        char[] password=(char[]) authentication.getCredentials();
        EmployerPassAuthToken employerPassAuthToken;
        UsersDetails employer=employerService.loadUserByUsername(String.valueOf(username));
        if (employer !=null && password != null && passwordEncoder.matches(String.valueOf(password),employer.getPassword())){
            employerPassAuthToken=new EmployerPassAuthToken(username,employer.getPassword(),employer.getAuthorities());
            employerPassAuthToken.setUsersDetails(employer);
        }else {
            employerPassAuthToken = new EmployerPassAuthToken(username, password);
            employerPassAuthToken.setAuthenticated(false);
        }
        return employerPassAuthToken;
    }

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

  1. security configuration class
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)

public class SecConfig {

    @Autowired
    UserPassAuthProvider userPassAuthProvider;
    @Autowired
    UserPassAuthFilter userPassAuthFilter;
    @Autowired
    TokenAuthFilter tokenAuthFilter;
    @Autowired
    AuthPathPrivilege userPrivilege;
    @Autowired
    AuthPath authPath;
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers(authPath.getAllNotAuthPath().iterator().next()).permitAll()
                .antMatchers(authPath.getAllAuthPath().iterator().next()).authenticated()
                .antMatchers(userPrivilege.getSuper_adminPrivilegePath().iterator().next()).hasAnyAuthority("SUPER_ADMIN")
                .antMatchers(userPrivilege.getAdminPrivilegePath().iterator().next()).hasAnyAuthority("ADMIN")
                .antMatchers(userPrivilege.getManagerPrivilegePath().iterator().next()).hasAnyAuthority("MANAGER")
                .antMatchers(userPrivilege.getUserPrivilegePath().iterator().next()).hasAnyAuthority("USER")
                .and().httpBasic()
                .and().sessionManagement().sessionCreationPolicy(STATELESS)
                .and().authenticationProvider(userPassAuthProvider)
                .addFilterAt(userPassAuthFilter , BasicAuthenticationFilter.class)
                .addFilterAfter(tokenAuthFilter , BasicAuthenticationFilter.class);

        return http.build();
    }

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

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

}

**Note: **

  1. Also using Intellij idea debugging it stuck on the same line of code authManager.authenticate(authentication) in user filter class
  2. The application didn't enter user authentication provider after creating employer provider.

Solution

  • In addition to Eleftheria's comment:

    You are asking Spring Security to provide you an AuthenticationManager bean by calling authenticationConfiguration.getAuthenticationManager().

    1. The AuthenticationConfiguration calls the AuthenticationManagerBuilder to create the AuthenticationManager, and it only creates the AuthenticationManager if there is only one AuthenticationProvider or only one UserDetailsService configured. If it is not able to create it, then a lazy initialization bean will be provided.

    2. When the application tries to use the AuthenticationManager for the first time, the lazy initialization bean will be triggered, invoking the method that creates the bean, which in your case is:

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
    
    1. That method, in turn, invokes AuthenticationConfiguration#getAuthenticationManager which will return a lazy initialization bean again since the AuthenticationManager cannot be created because of <1>, and the cycle continues.

    So, why Spring Security didn't initialize the AuthenticationManager in your case? Because you have 2 AuthenticationProviders, and there is no way for Spring Security to know how you want to combine them, that's why Spring Security backs off and lets you deal with the creation of AuthenticationManager.

    How do you fix it?

    Since Spring Security cannot create the bean for you, you would have to create the AuthenticationManager bean yourself. Here is how to do that in your scenario:

    @Bean
    public AuthenticationManager authenticationManager(List<AuthenticationProvider> myAuthenticationProviders) {
        return new ProviderManager(myAuthenticationProviders);
    }
    

    For folks that want to use a UserDetailsService, you can create a DaoAuthenticationProvider and set the necessary fields.