In our previous articles, we have implemented Basic Authentication, JWT Authentication, LDAP authentication with BASIC_AUTH
, JWT_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
JwtRequestFilter
will look for the token in the request header.- If token found, then it will validate the token and manually set the Authentication in Spring Security Context.
- Else, logs a warning and moves on to the next authentication
- LDAP Authentication Provider will validate the user credentials with the LDAP server.
- 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.
- Else, it moves on to the next authentication.
- Basic Authentication Provider will validate the user credentials with the Database using the
UserDetailsService
implementation.- If authentication is success, then the authentication principal will be set with the configured authorities in the security context and process completes.
- 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.
Hi, I’m in the process of studying SpringBoot, I’m going through the authenticate section and I’m interested in focusing on a multiple path and I found your article. I have a question and I would like to see if you can help me… when and where do you generate the test users? I mean “user” and “admin” with their respective passwords
SetupDataLoader is responsible for generating the test users during application startup.