Connect multiple authentication mechanisms Spring Boot Security

11,436

Solution 1

Spring Won't use more than one AuthenticationProvider to authenticate the request, so the first (in the ArrayList) AuthenticationProvider that support the Authentication object and successfully authenticate the request will be the only one used. in your case it's activeDirectoryLdapAuthenticationProvider.

instead of using ActiveDirectoryLdapAuthenticationProvider, you can use a custom AuthenticationProvider that delegates to LDAP and do additional checks:

    CustomerAuthenticationProvider implements AuthenticationProvider{
        privtae ActiveDirectoryLdapAuthenticationProvider  delegate; // add additional methods to initialize delegate during your configuration

          @Override
         public Authentication authenticate(Authentication auth) throws 
             AuthenticationException {
            Authentication  authentication= delegate.authenticate(auth);
            additionalChecks(authentication);
           return auth;
           }


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

        public void additionalCheck(Authentication authentication){
               // throw AuthenticationException when it's not allowed
        }

    }

Solution 2

That is not how an AuthenticationProvider works, only one will be consulted for authentication. Apparently you want to combine some information from LDAP and from the DB. For this you can configure a custom UserDetailsContextMapper and/or GrantedAuthoritiesMapper. The default implementation will use the information from LDAP to contruct the UserDetails and its GrantedAuthorities however you could implement a strategy which consults the database.

Another solution is to use the LdapUserDetailsService which allows you to use the regular DaoAuthenticationProvider. The name is misleading as it actually requires an UserDetailsService. This AuthenticationProvider does additional checks using the UserDetailsChecker, which by default checks some of the properties on the UserDetails, but can be extended with your additional checks.

NOTE: The LdapUserDetailsService uses plain LDAP so I don't know if that is applicable to the slightly different Active Directory approach!

A final solution could be to create a DelegatingAuthenticationProvider which extends from AbstractUserDetailsAuthenticationProvider so that you can reuse the logic in there to utilize the UserDetailsChecker. The retrieveUser method would then delegate to the actual ActiveDirectoryLdapAuthenticationProvider to do the authentication.

NOTE: Instead of extending the AbstractUserDetailsAuthenticationProvider you could of course also create a simpler version yourself.

All in all I suspect that creating a customized UserDetailsContextMapper would be the easiest and when not found in DB throw an UsernameNotFoundException. This way the normal flow still applies and you can reuse most of the existing infrastructure.

Solution 3

As sample work around on multiple authentication mechanism : find the code

@Configuration
@EnableWebSecurity
@Profile("container")
public class CustomWebSecurityConfig  extends WebSecurityConfigurerAdapter {

@Autowired
private AuthenticationProvider authenticationProvider;

@Autowired
private AuthenticationProvider authenticationProviderDB;

@Override
@Order(1)
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(authenticationProvider);
}

@Order(2)
protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(authenticationProviderDB);
}

@Override
  public void configure(WebSecurity web) throws Exception {
    web
      .ignoring()
         .antMatchers("/scripts/**","/styles/**","/images/**","/error/**");
  }

@Override
public void configure(HttpSecurity http) throws Exception {
    http
            .authorizeRequests()
            .antMatchers("/rest/**").authenticated()
            .antMatchers("/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .successHandler(new AuthenticationSuccessHandler() {
                @Override
                public void onAuthenticationSuccess(
                        HttpServletRequest request,
                        HttpServletResponse response,
                        Authentication a) throws IOException, ServletException {
                            //To change body of generated methods,
                            response.setStatus(HttpServletResponse.SC_OK);
                        }
            })
            .failureHandler(new AuthenticationFailureHandler() {

                @Override
                public void onAuthenticationFailure(
                        HttpServletRequest request,
                        HttpServletResponse response,
                        AuthenticationException ae) throws IOException, ServletException {
                            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                        }
            })
            .loginProcessingUrl("/access/login")
            .and()
            .logout()
            .logoutUrl("/access/logout")                
            .logoutSuccessHandler(new LogoutSuccessHandler() {
                @Override
                public void onLogoutSuccess(
                        HttpServletRequest request, 
                        HttpServletResponse response, 
                        Authentication a) throws IOException, ServletException {
                    response.setStatus(HttpServletResponse.SC_NO_CONTENT);
                }
            })
            .invalidateHttpSession(true)
            .and()
            .exceptionHandling()
            .authenticationEntryPoint(new Http403ForbiddenEntryPoint())
            .and()
            .csrf()//Disabled CSRF protection
            .disable();
    }
} 

configured two authentication providers in Spring Security

 <security:authentication-manager>
      <security:authentication-provider ref="AuthenticationProvider " />
      <security:authentication-provider ref="dbAuthenticationProvider" />
   </security:authentication-manager>

configuration which helps configure multiple authentication providers in java config.

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(authenticationProvider);
    auth.authenticationProvider(DBauthenticationProvider);
}


@Configuration
@EnableWebSecurity
public class XSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private LDAPAuthenticationProvider authenticationProvider;

    @Autowired
    private DBAuthenticationProvider dbauthenticationProvider;

    @Override
      public void configure(WebSecurity web) throws Exception {
        web
          .ignoring()
             .antMatchers("/scripts/**","/styles/**","/images/**","/error/**");
      }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider);
        auth.authenticationProvider(dbauthenticationProvider);

    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
        .authorizeRequests()
            .antMatchers("/","/logout").permitAll()
            .antMatchers("/admin").hasRole("ADMIN")         
            .anyRequest().authenticated()
            .and()
        .formLogin()
            .loginPage("/index")
            .loginProcessingUrl("/perform_login")
            .usernameParameter("user")
            .passwordParameter("password")
            .failureUrl("/index?failed=true")
            .defaultSuccessUrl("/test",true)
            .permitAll()
            .and()
         .logout().logoutUrl("/logout")
                  .logoutSuccessUrl("/index?logout=true").permitAll()
            .and()
            .exceptionHandling().accessDeniedPage("/error");
    }

}

objectPostProcessor inside the configure method need AuthenticationManagerBuilder to actually build the object before we can access and change the order of the providers

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
      auth.jdbcAuthentication().dataSource(dataSource)
           .passwordEncoder(new BCryptPasswordEncoder());
      auth.authenticationProvider(new CustomAuthenticationProvider(this.dataSource));

      auth.objectPostProcessor(new ObjectPostProcessor<Object>() {
        @Override
        public <O> O postProcess(O object) {
            ProviderManager providerManager = (ProviderManager) object;
            Collections.swap(providerManager.getProviders(), 0, 1);
            return object;
        }
    });
}
Share:
11,436

Related videos on Youtube

Rüdiger
Author by

Rüdiger

Updated on September 15, 2022

Comments

  • Rüdiger
    Rüdiger over 1 year

    I have a security configuration for my application that authenticates the user via LDAP. This works out pretty fine, but now I'd like to add another AuthenticationProvider that does some more checks on the user that tries authenticate. So I tried to add a DbAuthenticationProvider that (for testing purposes) always denies the access. So when I am trying to log in with my domain account (that works for the activeDirectoryLdapAuthenticationProvider) I am not able to access the page because the second provider fails the authentication.

    To accomplish this goal, I used the following code:

    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Value("${ad.domain}")
        private String AD_DOMAIN;
    
        @Value("${ad.url}")
        private String AD_URL;
    
        @Autowired
        UserRoleComponent userRoleComponent;
    
        @Autowired
        DbAuthenticationProvider dbAuthenticationProvider;
    
        private final Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class);
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            this.logger.info("Verify logging level");
            http.authorizeRequests().anyRequest().fullyAuthenticated().and().formLogin()
                    .successHandler(new CustomAuthenticationSuccessHandler()).and().httpBasic().and().logout()
                    .logoutUrl("/logout").invalidateHttpSession(true).deleteCookies("JSESSIONID");
            http.formLogin().defaultSuccessUrl("/", true);
        }
    
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.authenticationProvider(activeDirectoryLdapAuthenticationProvider());
            auth.authenticationProvider(dbAuthenticationProvider);
        }
    
        @Bean
        public AuthenticationManager authenticationManager() {
            return new ProviderManager(Arrays.asList(activeDirectoryLdapAuthenticationProvider(), dbAuthenticationProvider));
        }
    
        @Bean
        public AuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
            ActiveDirectoryLdapAuthenticationProvider provider = new ActiveDirectoryLdapAuthenticationProvider(AD_DOMAIN,
                    AD_URL);
            provider.setConvertSubErrorCodesToExceptions(true);
            provider.setUseAuthenticationRequestCredentials(true);
            return provider;
        }
    }
    

    And this is my DbAuthenticationProvider:

    @Component
    public class DbAuthenticationProvider implements AuthenticationProvider {
    
        Logger logger = LoggerFactory.getLogger(DbAuthenticationProvider.class);
    
        @Override
        public Authentication authenticate(Authentication auth) throws AuthenticationException {
            auth.setAuthenticated(false);
            this.logger.info("Got initialized");
            return auth;
        }
    
        @Override
        public boolean supports(Class<?> authentication) {
            return true;
        }
    
    }
    

    Sadly I am able to log in (the access is not denied as I expected it to be). Did I miss out something?

    • Dominik
      Dominik
      just as a hint: you may want to look up redhats keycloak, which could fullfill your requirements and more out of the box and has a great spring integration.
  • Rüdiger
    Rüdiger about 5 years
    That's a great approach, sadly the ActiveDirectoryLdapAuthenticationProvider is a final class, can I somehow workaround that?
  • learner
    learner over 3 years
    Can you please tell me how to do this part "add additional methods to initialize delegate during your configuration"? Your approach is the best one