Description

  • Spring boot version: 2.7.1
  • JDK version: 1.11

I'm moving from old authentication style to the new authentication style based on the article published in the blog spring-security-without-the-websecurityconfigureradapter -> before adding the issue I have looking in stackoverflow for similar issue, here in closed issues, dead loops etc... but I have not been able to find anything in the same direction.

Everythings goes fine, except when I introduce bad credentials, then the application seems go into a loop until it is raised an java.lang.StackOverflowError: null ( here the full error stack trace ) error.txt

I have created a sample code at https://github.com/darkman97i/spring-security-test ( in the sample I'm using two providers in memory and jdbc. Also I included h2 database with users credentials into for a quick test ).

Thanks for your time

Comment From: wilkinsona

Your use of the shared authentication manager builder results in a provider manager that is using itself as a parent. This results in an infinite loop as when the login fails, the provider manager queries its parent to see if it can authenticate.

This problem isn't directly related to Spring Boot as it's a Spring Security usage question. Your sample works if I inject an ObjectPostProcessor and create a new AuthenticationManagerBuilder:

@Bean
public AuthenticationManager authenticationManager(HttpSecurity http, ObjectPostProcessor<Object> objectPostProcessor) throws Exception {
    AuthenticationManagerBuilder authenticationManagerBuilder = new AuthenticationManagerBuilder(objectPostProcessor);

However, I'm not sure if this is the best way to solve the problem. Unfortunately, there are no hits on AuthenticationManagerBuilder in the Spring Security reference documentation so it doesn't appear to offer any guidance.

Can you please open a Spring Security issue that references this issue, and comment here with a link to it? That will bring the problem to the Security team's attention. They could perhaps prohibit a cycle among the ProviderManager ancestors, make some updates to the documentation, or something else that they feel is more appropriate.

Comment From: darkman97i

@wilkinsona First, sorry for my late answer. The issue opened at spring-security is still open but the spring-security team suggested me another direction and it worked perfectly. I share complete solution it worked for me, maybe help other users.

Thanks a lot for your help and time invested with us, for me, can close the issue.

Related application.properties file used in the class

# Authentication
okm.authentication.supervisor=false
okm.authentication.database=true
okm.authentication.ldap=false
okm.authentication.config=classpath:/openkm.xml

# LDAP
ldap.server=ldap://192.168.1.40
ldap.manager.distinguished.name=CN=Administrator,CN=Users,DC=openkm,DC=local
ldap.manager.password=*secret*
ldap.base=DC=openkm,DC=local
ldap.role.attribute=cn
ldap.user.search.filter=sAMAccountName={0}
ldap.role.search.filter=member={0}
ldap.role.prefix=
ldap.referral=follow
ldap.ignore.partial.result.exception=false

The SecurityConfigManager class ( The @Qualifiers are optional but I prefer to set it to get more control )

package com.openkm.config;

import com.openkm.core.Config;
import org.apache.commons.lang3.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.AbstractEnvironment;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
import org.springframework.security.ldap.authentication.BindAuthenticator;
import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
import org.springframework.security.ldap.userdetails.LdapUserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.JdbcUserDetailsManager;

import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Configuration
public class SecurityConfigManager {
    private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class);
    private static final String PROPERTY_KEY_AUTH_SUPERVISOR = "okm.authentication.supervisor";
    private static final String PROPERTY_KEY_AUTH_DATABASE = "okm.authentication.database";
    public static final String PROPERTY_KEY_AUTH_LDAP = "okm.authentication.ldap";
    public static final String PROPERTY_KEY_AUTH_CONFIG = "okm.authentication.config";
    private String authModules = "";

    @Autowired
    private AbstractEnvironment env;

    @Autowired
    private Config cfg;

    @Bean
    @Qualifier("passwordEncoder")
    // Set the default Password encoder what will be used by JdbcUserDetailsManager otherwise authentication does not works
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    GrantedAuthorityDefaults grantedAuthorityDefaults() {
        // Remove the ROLE_ prefix
        return new GrantedAuthorityDefaults(cfg.DEFAULT_ROLE_PREFIX);
    }

    @Bean
    @ConditionalOnProperty(name = PROPERTY_KEY_AUTH_CONFIG, havingValue = "classpath:/openkm.xml", matchIfMissing = true)
    AuthenticationManager apiAuthenticationManager(@Qualifier("openkmJdbcAuthenticationProvider") AuthenticationProvider jdbcAuthenticationProvider,
                                                   @Qualifier("openkmInMemoryAuthenticationProvider") AuthenticationProvider inMemoryAuthenticationProvider,
                                                   @Qualifier("openkmLdapAuthenticationProvider") LdapAuthenticationProvider ldapAuthenticationProvider) {
        List<AuthenticationProvider> providers = new ArrayList<>();
        // Supervisor
        if ("true".equals(env.getProperty(PROPERTY_KEY_AUTH_SUPERVISOR))) {
            providers.add(inMemoryAuthenticationProvider);
        }
        // Internal database
        if ("true".equals(env.getProperty(PROPERTY_KEY_AUTH_DATABASE))) {
            authModules += "db;";
            providers.add(jdbcAuthenticationProvider);
        }
        //      // OpenKM ldap connection
        if ("true".equals(env.getProperty(PROPERTY_KEY_AUTH_LDAP))) {
            authModules += "ldap;";
            providers.add(ldapAuthenticationProvider);
        }
        return new ProviderManager(providers);
    }

    // IN MEMORY AUTHENTICATION PROVIDER
    @Bean("openkmInMemoryAuthenticationProvider")
    public AuthenticationProvider inMemoryAuthenticationProvider(@Qualifier("openkmInMemoryUserDetailsManager") InMemoryUserDetailsManager inMemoryUserDetailsManager,
                                                                 PasswordEncoder passwordEncoder) {
        DaoAuthenticationProvider inMemoryAuthenticationProvider = new DaoAuthenticationProvider();
        inMemoryAuthenticationProvider.setUserDetailsService(inMemoryUserDetailsManager);
        inMemoryAuthenticationProvider.setPasswordEncoder(passwordEncoder);
        return inMemoryAuthenticationProvider;
    }

    @Bean("openkmInMemoryUserDetailsManager")
    public InMemoryUserDetailsManager inMemoryUserDetailsManager(PasswordEncoder passwordEncoder) {
        String adminUser = cfg.DEFAULT_ADMIN_USER; // Usually the okmAdmin user, but not always
        String roleAdmin = cfg.DEFAULT_ADMIN_ROLE; // Must remove ROLE_ at the beginning because by default is set by inMemoryAuthentication

        if (roleAdmin.startsWith("ROLE_")) {
            roleAdmin = roleAdmin.substring(5);
        }

        String passwd = RandomStringUtils.randomAlphanumeric(18);
        log.info("*****************************************************");
        log.info("* Generated supervisor user: {}, password: {}", adminUser, passwd);
        log.info("*****************************************************");

        UserDetails user = User.withUsername(adminUser)
                .passwordEncoder(passwordEncoder::encode)
                .password(passwd)
                .roles(roleAdmin)
                .build();
        return new InMemoryUserDetailsManager(user);
    }

    // JDBC AUTHENTICATION PROVIDER
    @Bean("openkmJdbcAuthenticationProvider")
    public AuthenticationProvider jdbcAuthenticationProvider(@Qualifier("openkmJdbcUserDetailsManager") JdbcUserDetailsManager jdbcUserDetailsManager,
                                                             PasswordEncoder passwordEncoder) {
        DaoAuthenticationProvider jdbcAuthenticationProvider = new DaoAuthenticationProvider();
        jdbcAuthenticationProvider.setUserDetailsService(jdbcUserDetailsManager);
        jdbcAuthenticationProvider.setPasswordEncoder(passwordEncoder);
        return jdbcAuthenticationProvider;
    }

    @Bean("openkmJdbcUserDetailsManager")
    public JdbcUserDetailsManager jdbcUserDetailsManager(DataSource dataSource) {
        JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager(dataSource);
        jdbcUserDetailsManager.setUsersByUsernameQuery("select USR_ID, USR_PASSWORD, 1 from OKM_USER where USR_ID=? and USR_ACTIVE='T'");
        jdbcUserDetailsManager.setAuthoritiesByUsernameQuery("select UR_USER, UR_ROLE from OKM_USER_ROLE where UR_USER=?");
        return jdbcUserDetailsManager;
    }

    // LDAP AUTHENTICATION
    @Bean("openkmLdapAuthenticationProvider")
    public LdapAuthenticationProvider getLdapAuthenticationProvider(@Qualifier("openkmBindAuthenticator") BindAuthenticator bindAuthenticator,
                                                                    @Qualifier("openkmLdapAuthoritiesPopulator") LdapAuthoritiesPopulator ldapAuthoritiesPopulator) {
        LdapAuthenticationProvider ldapAuthenticationProvider = new LdapAuthenticationProvider(bindAuthenticator, ldapAuthoritiesPopulator);
        return ldapAuthenticationProvider;
    }

    @Bean("openkmBindAuthenticator")
    public BindAuthenticator getBindAuthenticator(@Qualifier("openkmFilterBasedLdapUserSearch") FilterBasedLdapUserSearch filterBasedLdapUserSearch,
                                                  @Qualifier("openkmLdapContextSource") DefaultSpringSecurityContextSource defaultSpringSecurityContextSource) {
        BindAuthenticator bindAuthenticator = new BindAuthenticator(defaultSpringSecurityContextSource);
        bindAuthenticator.setUserSearch(filterBasedLdapUserSearch);
        return bindAuthenticator;
    }

    @Bean("openkmLdapContextSource")
    public DefaultSpringSecurityContextSource getLdapContextSource() {
        String ldapUrl = env.getProperty("ldap.server");
        String managerDn = env.getProperty("ldap.manager.distinguished.name");
        String managerPassword = env.getProperty("ldap.manager.password");
        String referral = env.getProperty("ldap.referral");
        DefaultSpringSecurityContextSource defaultSpringSecurityContextSource = new DefaultSpringSecurityContextSource(ldapUrl);
        defaultSpringSecurityContextSource.setUserDn(managerDn);
        defaultSpringSecurityContextSource.setPassword(managerPassword);

        if (referral.isEmpty()) {
            referral = "follow";
        }

        Map<String, Object> baseEnvironmentProperties = new HashMap<>();
        baseEnvironmentProperties.put("java.naming.referral", referral);
        defaultSpringSecurityContextSource.setBaseEnvironmentProperties(baseEnvironmentProperties);
        return defaultSpringSecurityContextSource;
    }

    @Bean("openkmLdapAuthoritiesPopulator")
    public LdapAuthoritiesPopulator getLdapAuthoritiesPopulator(@Qualifier("openkmLdapContextSource") DefaultSpringSecurityContextSource defaultSpringSecurityContextSource) {
        String groupSearchBase = env.getProperty("ldap.base");
        String roleAttribute = env.getProperty("ldap.role.attribute");
        String roleSearchFilter = env.getProperty("ldap.role.search.filter");
        String rolePrefix = env.getProperty("ldap.role.prefix");
        DefaultLdapAuthoritiesPopulator authoritiesPopulator = new DefaultLdapAuthoritiesPopulator(defaultSpringSecurityContextSource, groupSearchBase);
        authoritiesPopulator.setGroupRoleAttribute(roleAttribute);
        authoritiesPopulator.setGroupSearchFilter(roleSearchFilter);
        authoritiesPopulator.setSearchSubtree(true);
        authoritiesPopulator.setConvertToUpperCase(false);
        authoritiesPopulator.setRolePrefix(rolePrefix);

        if (Boolean.parseBoolean(env.getProperty("ldap.ignore.partial.result.exception"))) {
            authoritiesPopulator.setIgnorePartialResultException(true);;
        }

        return authoritiesPopulator;
    }

    @Bean("openkmFilterBasedLdapUserSearch")
    public FilterBasedLdapUserSearch getFilterBasedLdapUserSearch(@Qualifier("openkmLdapContextSource") DefaultSpringSecurityContextSource defaultSpringSecurityContextSource) {
        String ldapBase = env.getProperty("ldap.base");
        String userSearchFilter = env.getProperty("ldap.user.search.filter");
        FilterBasedLdapUserSearch ldapUserSearch = new FilterBasedLdapUserSearch(ldapBase, userSearchFilter, defaultSpringSecurityContextSource);
        ldapUserSearch.setSearchSubtree(true);
        return ldapUserSearch;
    }

    @Bean("openkmLdapUserDetailsService")
    @ConditionalOnProperty(name = PROPERTY_KEY_AUTH_LDAP, havingValue = "true", matchIfMissing = false)
    public UserDetailsService getLdapUserDetailsService(@Qualifier("openkmFilterBasedLdapUserSearch") FilterBasedLdapUserSearch filterBasedLdapUserSearch,
                                                        @Qualifier("openkmLdapAuthoritiesPopulator") LdapAuthoritiesPopulator ldapAuthoritiesPopulator) {
        return new LdapUserDetailsService(filterBasedLdapUserSearch, ldapAuthoritiesPopulator);
    }

    public String getAuthModules() {
        return authModules;
    }
}