1. Overview
In this quick article, we’re going to focus on using multiple mechanisms to authenticate users in Spring Security.
We’ll do that by configuring multiple authentication providers.
2. Authentication Providers
An AuthenticationProvider is an abstraction for fetching user information from a specific repository (like a database, LDAP, custom third party source, etc. ). It uses the fetched user information to validate the supplied credentials.
Simply put, when multiple authentication providers are defined, the providers will be queried in the order they’re declared.
For a quick demonstration, we’ll configure two authentication providers – a custom authentication provider and an in-memory authentication provider.
3. Maven Dependencies
Let’s first add the necessary Spring Security dependencies into our web application:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
And, without Spring Boot:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>6.1.5</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>6.1.5</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>6.1.5</version>
</dependency>
The latest version of these dependencies can be found at spring-security-web, spring-security-core, and spring-security-config.
4. Custom Authentication Provider
Let’s now create a custom authentication provider by implementing the AuthneticationProvider interface*.*
We’re going to implement the authenticate method – which attempts the authentication. The input Authentication object contains the username and password credentials supplied by the user.
The authenticate method returns a fully populated Authentication object if the authentication is successful. If authentication fails, it throws an exception of type AuthenticationException:
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication auth)
throws AuthenticationException {
String username = auth.getName();
String password = auth.getCredentials()
.toString();
if ("externaluser".equals(username) && "pass".equals(password)) {
return new UsernamePasswordAuthenticationToken
(username, password, Collections.emptyList());
} else {
throw new
BadCredentialsException("External system authentication failed");
}
}
@Override
public boolean supports(Class<?> auth) {
return auth.equals(UsernamePasswordAuthenticationToken.class);
}
}
Naturally, this is a simple implementation for the purpose of our example here.
5. Configuring Multiple Authentication Providers
Let’s now add the CustomAuthenticationProvider and an in-memory authentication provider to our Spring Security configuration.
5.1. Java Configuration
In our configuration class, let’s now create and add the authentication providers using the AuthenticationManagerBuilder.
First, the CustomAuthenticationProvider and then, an in-memory authentication provider by using inMemoryAuthentication().
We are also making sure that access to the URL pattern “*/api/***” needs to be authenticated:
@Configuration
@EnableWebSecurity
public class MultipleAuthProvidersSecurityConfig {
@Autowired
CustomAuthenticationProvider customAuthProvider;
@Bean
public AuthenticationManager authManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.authenticationProvider(customAuthProvider);
authenticationManagerBuilder.inMemoryAuthentication()
.withUser("memuser")
.password(passwordEncoder().encode("pass"))
.roles("USER");
return authenticationManagerBuilder.build();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authManager, HandlerMappingIntrospector introspector) throws Exception {
MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);
http.httpBasic(Customizer.withDefaults())
.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry ->
authorizationManagerRequestMatcherRegistry
.requestMatchers(PathRequest.toH2Console()).authenticated()
.requestMatchers(mvcMatcherBuilder.pattern("/api/**")).authenticated())
.authenticationManager(authManager);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
5.2. XML Configuration
Alternatively, if we want to use XML configuration instead of Java configuration:
<security:authentication-manager>
<security:authentication-provider>
<security:user-service>
<security:user name="memuser" password="pass"
authorities="ROLE_USER" />
</security:user-service>
</security:authentication-provider>
<security:authentication-provider
ref="customAuthenticationProvider" />
</security:authentication-manager>
<security:http>
<security:http-basic />
<security:intercept-url pattern="/api/**"
access="isAuthenticated()" />
</security:http>
6. The Application
Next, let’s create a simple REST endpoint that is secured by our two authentication providers.
To access this endpoint, a valid username and password must be supplied. Our authentication providers will validate the credentials and determine whether to allow access or not:
@RestController
public class MultipleAuthController {
@GetMapping("/api/ping")
public String getPing() {
return "OK";
}
}
7. Testing
Finally, let’s now test the access to our secure application. Access will be allowed only if valid credentials are supplied:
@Autowired
private TestRestTemplate restTemplate;
@Test
public void givenMemUsers_whenGetPingWithValidUser_thenOk() {
ResponseEntity<String> result
= makeRestCallToGetPing("memuser", "pass");
assertThat(result.getStatusCode().value()).isEqualTo(200);
assertThat(result.getBody()).isEqualTo("OK");
}
@Test
public void givenExternalUsers_whenGetPingWithValidUser_thenOK() {
ResponseEntity<String> result
= makeRestCallToGetPing("externaluser", "pass");
assertThat(result.getStatusCode().value()).isEqualTo(200);
assertThat(result.getBody()).isEqualTo("OK");
}
@Test
public void givenAuthProviders_whenGetPingWithNoCred_then401() {
ResponseEntity<String> result = makeRestCallToGetPing();
assertThat(result.getStatusCodeValue()).isEqualTo(401);
}
@Test
public void givenAuthProviders_whenGetPingWithBadCred_then401() {
ResponseEntity<String> result
= makeRestCallToGetPing("user", "bad_password");
assertThat(result.getStatusCode().value()).isEqualTo(401);
}
private ResponseEntity<String>
makeRestCallToGetPing(String username, String password) {
return restTemplate.withBasicAuth(username, password)
.getForEntity("/api/ping", String.class, Collections.emptyMap());
}
private ResponseEntity<String> makeRestCallToGetPing() {
return restTemplate
.getForEntity("/api/ping", String.class, Collections.emptyMap());
}
8. Conclusion
In this quick tutorial, we’ve seen how multiple authentication providers can be configured in Spring Security. We have secured a simple application using a custom authentication provider and an in-memory authentication provider.
And we’ve also written tests to verify that the access to our application requires credentials that can be validated by at least one of our authentication providers.
As always, the full source code of the implementation can be found over on GitHub.