Connect multiple authentication mechanisms Spring Boot Security
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;
}
});
}
Related videos on Youtube
Rüdiger
Updated on September 15, 2022Comments
-
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 anotherAuthenticationProvider
that does some more checks on the user that tries authenticate. So I tried to add aDbAuthenticationProvider
that (for testing purposes) always denies the access. So when I am trying to log in with my domain account (that works for theactiveDirectoryLdapAuthenticationProvider
) 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?
-
Dominikjust 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 about 5 yearsThat's a great approach, sadly the
ActiveDirectoryLdapAuthenticationProvider
is a final class, can I somehow workaround that? -
learner over 3 yearsCan you please tell me how to do this part "add additional methods to initialize delegate during your configuration"? Your approach is the best one