How to Configure Spring Security with Multiple Authentication Providers

In our previous articles, we have implemented Basic AuthenticationJWT AuthenticationLDAP authentication with BASIC_AUTHJWT_AUTH and LDAP_AUTH profiles respectively. Also, we have seen how to disable Spring Security with NO_AUTH profile.

However, sometimes our application might need to support multiple authentications in a single profile. For example, if an application is being used by internal users as well as external users, then LDAP authentication can be used for internal users and Basic / JWT token-based authentication can be used for external users. Hence, we are gonna create a MULTI_AUTH profile and configure multiple authentication providers in Spring Security. So that, a user can log in with Basic Authentication or LDAP Authentication or JWT token passed in the Authorization header of the request.

Configure Spring Security with Multiple Authentication Providers

Profiles.java

Create a constant for Multiple Authentication profile

package com.javachinna.config;

public class Profiles {

	private Profiles() {
	}

	public static final String NO_AUTH = "noauth";
	public static final String BASIC_AUTH = "basicauth";
	public static final String JWT_AUTH = "jwtauth";
	public static final String LDAP_AUTH = "ldapauth";
	public static final String MULTI_AUTH = "multiauth";
}

MultiAuthSecurityConfig.java

@Profile(Profiles.MULTI_AUTH) annotation is used to enable multiple authentication providers only when the application is run with “multiauth” profile.

MultiAuthSecurityConfig class extends the WebSecurityConfigurerAdapter to configure Spring Security with multiple authentication providers.

When multiple authentication providers are defined, the providers will be queried in the order they’re declared. So if one authentication fails, then it will move on to the next authentication provider.

package com.javachinna.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.LdapShaPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.javachinna.model.Role;

import lombok.RequiredArgsConstructor;

@Profile(Profiles.MULTI_AUTH)
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class MultiAuthSecurityConfig extends WebSecurityConfigurerAdapter {

	private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
	private final UserDetailsService jwtUserDetailsService;
	private final JwtRequestFilter jwtRequestFilter;
	private final LdapUserAuthoritiesPopulator ldapUserAuthoritiesPopulator;

	@Autowired
	public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
		// Returns LdapAuthenticationProviderConfigurer to allow customization of the
		// LDAP authentication
		auth.ldapAuthentication()
				// Pass the LDAP patterns for finding the username.
				// The key "{0}" will be substituted with the username
				.userDnPatterns("uid={0},ou=users")
				// Pass search base as argument for group membership searches.
				.groupSearchBase("ou=groups")
				// Configures base LDAP path context source
				.contextSource().url("ldap://localhost:10389/dc=javachinna,dc=com")
				// DN of the user who will bind to the LDAP server to perform the search
				.managerDn("uid=admin,ou=system")
				// Password of the user who will bind to the LDAP server to perform the search
				.managerPassword("secret").and()
				// Configures LDAP compare operation of the user password to authenticate
				.passwordCompare().passwordEncoder(new LdapShaPasswordEncoder())
				// Specifies the attribute in the directory which contains the user password.
				// Defaults to "userPassword".
				.passwordAttribute("userPassword").and()
				// Populates the user roles by LDAP user name from database
				.ldapAuthoritiesPopulator(ldapUserAuthoritiesPopulator);
		// Basic / JWT authentication
		auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
	}

	@Override
	protected void configure(HttpSecurity httpSecurity) throws Exception {
		// Disable CSRF
		httpSecurity.csrf().disable()
				// Only admin can perform HTTP delete operation
				.authorizeRequests().antMatchers(HttpMethod.DELETE).hasRole(Role.ADMIN)
				// any authenticated user can perform all other operations
				.antMatchers("/products/**").hasAnyRole(Role.ADMIN, Role.USER).and().httpBasic()
				// Permit all other request without authentication
				.and().authorizeRequests().anyRequest().permitAll()
				// Reject every unauthenticated request and send error code 401.
				.and().exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
				// We don't need sessions to be created.
				.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

		// Add a filter to validate the tokens with every request
		httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
	}
	
	@Bean
	@Override
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}
	
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
}
Authentication Order
  1. JwtRequestFilter will look for the token in the request header.
    1. If token found, then it will validate the token and manually set the Authentication in Spring Security Context.
    2. Else, logs a warning and moves on to the next authentication
  2. LDAP Authentication Provider will validate the user credentials with the LDAP server.
    1. If authentication is success, then it will try to fetch user authorities by username from the database. If authorities found, then it will be assigned to the user. Else, default user role will be assigned and the authentication process completes.
    2. Else, it moves on to the next authentication.
  3. Basic Authentication Provider will validate the user credentials with the Database using the UserDetailsService implementation.
    1. If authentication is success, then the authentication principal will be set with the configured authorities in the security context and process completes.
    2. Else, authentication is failed and process completes.

LdapUserAuthoritiesPopulator.java

Modified this class to assign a default user role if the user authorities were not found for the given LDAP username in the database.

package com.javachinna.config;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;

import org.springframework.ldap.core.DirContextOperations;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
import org.springframework.stereotype.Component;

import com.javachinna.model.Role;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RequiredArgsConstructor
@Component
public class LdapUserAuthoritiesPopulator implements LdapAuthoritiesPopulator {

	private final UserDetailsService userDetailsService;

	@Override
	public Collection<? extends GrantedAuthority> getGrantedAuthorities(DirContextOperations userData, String username) {
		Collection<? extends GrantedAuthority> authorities = new HashSet<>();
		try {
			authorities = userDetailsService.loadUserByUsername(username).getAuthorities();
		} catch (Exception e) {
			log.warn("Unable to fetch the user authorities from the database. Hence, assigning default user role");
			authorities = Arrays.asList(new SimpleGrantedAuthority(Role.ROLE_USER));
		}
		return authorities;
	}
}

Add MULTI_AUTH profile

We need to add the MULTI_AUTH profile to the following JWT filter and controller so that they will be enabled for this profile as well.

JwtRequestFilter.java

@Component
@Profile({Profiles.JWT_AUTH, Profiles.MULTI_AUTH})
@RequiredArgsConstructor
public class JwtRequestFilter extends OncePerRequestFilter {

JwtAuthenticationController.java

@Profile({Profiles.JWT_AUTH, Profiles.MULTI_AUTH})
@RestController
@CrossOrigin
@RequiredArgsConstructor
public class JwtAuthenticationController {

Run with Multi Auth Profile

You can run the application using mvn spring-boot:run -Dspring-boot.run.profiles=multiauth and hit the URL http://localhost:8080/swagger-ui.html in the browser. You should be able to execute the services with any of the above-mentioned authentications.

Source Code

As always, you can get the source code from Github below

https://github.com/JavaChinna/spring-boot-rest-multi-auth

Conclusion

That’s all folks! In this article, you’ve learned how to configure multiple authentication providers in a single profile for Spring Boot RESTful services.

I hope you enjoyed this article. Thank you for reading.

Leave a Reply