Computer scienceBackendSpring BootSpring SecuritySpring Security internals

AuthenticationManager

12 minutes read

The AuthenticationManager is a primary interface in the Spring Security framework. It manages essential functions of the authentication process. You use it to handle and verify user credentials, along with applying authentication rules within the Spring application.

The role of AuthenticationManager

The AuthenticationManager oversees the entire authentication flow:

  • The central authentication point: It acts as a single entrance for authentication requests in the Spring application, thereby simplifying authorization management.

  • Delegating authentication to authentication providers: When a user tries to access a secured resource, an authentication object carrying the request gets sent to the AuthenticationManager. The AuthenticationManager then delegates this request to its list of AuthenticationProviders; each tries to authenticate the request based on their capabilities.

  • Decision making: The AuthenticationManager decides the outcome each time an authentication attempt is made. If an AuthenticationProvider successfully authenticates the request, it returns a fully authenticated authentication object. If every provider fails, the AuthenticationManager throws an AuthenticationException.

Interface method

The AuthenticationManager interface only contains a single method that needs implementation:

public interface AuthenticationManager {
	Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

When invoking its authenticate() function, you might encounter one of three potential outcomes:

  1. Successful Authentication: It returns an Authentication object, usually marked with the authenticated flag as true. This outcome verifies the successful authentication of user credentials, confirming that the provided details coincide with a recognized user. The Authentication object represents the principal's identity and credentials, including their allowed authorities.

  2. Authentication Exception: If the AuthenticationManager deduces that the provided credentials are not valid, it throws an AuthenticationException. This exception, which is a type of runtime error, signifies an unsuccessful authentication attempt due to incorrect user credentials.

  3. Indecision: In situations where the AuthenticationManager cannot definitively decide the validity of given credentials, it might return null. This reflects an ambiguous state, neither verifying nor rejecting the user's authenticity.

Custom AuthenticationManager

First, you need to create a new Kotlin class that implements the AuthenticationManager interface. For this example, we'll create a straightforward manager that authenticates a user with a pre-set username and password. However, in a real-world situation, you would typically check against a database or another user store:

Java
@Component
public class CustomAuthenticationManager implements AuthenticationManager {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getPrincipal().toString();
        String password = authentication.getCredentials().toString();

        if ("admin".equals(username) && "password".equals(password)) {
            return new UsernamePasswordAuthenticationToken(username, password, new ArrayList<>());
        }

        throw new BadCredentialsException("Authentication failed for " + username);
    }
}
Kotlin
@Component
class CustomAuthenticationManager : AuthenticationManager {
    override fun authenticate(authentication: Authentication): Authentication {
        val username = authentication.principal.toString()
        val password = authentication.credentials.toString()

        if ("admin" == username && "password" == password) {
            return UsernamePasswordAuthenticationToken(username, password, listOf())
        }
        throw BadCredentialsException("Authentication failed for $username")
    }
}

Note that the authenticate method does not return null. In this case, the AuthenticationManager can clearly determine the user's authentication status, thereby ruling out an indecisive state.

After creating your CustomAuthenticationManager, you need to incorporate it into your Spring Security configuration. Adjust your SecurityConfig class to use the custom authentication manager:

Java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomAuthenticationManager authenticationManager;

    public SecurityConfig(CustomAuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(requests -> requests.anyRequest().authenticated());
        http.httpBasic(Customizer.withDefaults());
        http.authenticationManager(authenticationManager);
        return http.build();
    }
}
Kotlin
@Configuration
@EnableWebSecurity
class SecurityConfig(
    private val authenticationManager: CustomAuthenticationManager
) {
    @Bean
    fun configure(http: HttpSecurity): SecurityFilterChain {
        http.authorizeHttpRequests { requests -> requests.anyRequest().authenticated() }
        http.httpBasic(Customizer.withDefaults())
        http.authenticationManager(authenticationManager)
        return http.build()
    }
}

To test the security configuration, we need to add some restrictions to our application:

  • authorizeHttpRequests(): This method specifies the authorization rules for HTTP requests. By calling it, you establish the guidelines for which HTTP requests require authentication and which do not.

  • requests -> requests.anyRequest().authenticated(): This lambda expression outlines the actual authorization policy. anyRequest() is a method that matches any HTTP request. When combined with authenticated(), it stipulates that every request to the application must be authenticated. In other words, the user must log in to access any part of the application.

  • httpBasic(): This method sets up HTTP Basic Authentication, which is a simple authentication scheme that sends user credentials in the HTTP header. It's commonly employed for basic username/password authentication, usually in combination with HTTPS to ensure encryption.

  • Customizer.withDefaults(): This convenience method applies the default configuration for HTTP Basic Authentication. The default setup includes establishing the required authentication filter and challenge entry point, which prompts the user for their username and password.

ProviderManager

The ProviderManager is a default implementation of the AuthenticationManager that works in conjunction with a series of AuthenticationProvider instances. Each AuthenticationProvider within this chain is responsible for either authenticating, rejecting, or passing the authentication request to the next provider. If all providers fail to authenticate, the process ends with a ProviderNotFoundException. This error signifies that the types of supported authentication do not match the given Authentication type.

Each AuthenticationProvider involved in a ProviderManager specializes in handling a particular form of authentication. This modular approach allows the ProviderManager to support various authentication methods, ranging from username/password confirmation to complex mechanisms like SAML assertions. SAML, an acronym for Security Assertion Markup Language, is an open standard for exchanging authentication and authorization data between parties, allowing users to log in once and access multiple applications without needing to re-enter their credentials.

Provider manager flow

When an authentication request is received, typically as an Authentication object containing details like username and password, the ProviderManager iterates through its list of AuthenticationProvider instances to find one that can manage the request:

  1. Every AuthenticationProvider has a supports(Class<?> authentication) method. The ProviderManager uses this method to ascertain if each AuthenticationProvider can handle the specific type of Authentication object it has received. For instance, a provider that handles username and password authentication would return true when asked if it supports the UsernamePasswordAuthenticationToken class.

  2. When the ProviderManager finds an AuthenticationProvider that supports the Authentication interface type, it calls the authenticate method on that provider.

  3. Inside its authenticate method, the AuthenticationProvider carries out necessary checks, such as verifying the username and password against a database. If the authentication succeeds, the provider creates and returns a fully populated Authentication object, encompassing the user's authorities (roles).

Custom AuthenticationProvider

Let's create a simple Authentication Provider implementation, which will verify the username and password:

Java
@Component
public class CustomAuthenticationManager implements AuthenticationManager {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getPrincipal().toString();
        String password = authentication.getCredentials().toString();

        if ("admin".equals(username) && "password".equals(password)) {
            return new UsernamePasswordAuthenticationToken(username, password, new ArrayList<>());
        }

        throw new BadCredentialsException("Authentication failed for " + username);
    }
}
Kotlin
@Component
class CustomAuthenticationProvider : AuthenticationProvider {
    override fun authenticate(authentication: Authentication): Authentication {
        val username = authentication.principal.toString()
        val password = authentication.credentials.toString()

        if ("admin" == username && "password" == password) {
            return UsernamePasswordAuthenticationToken(username, password, listOf())
        }
        throw BadCredentialsException("Authentication failed for $username")
    }

    override fun supports(auth: Class<*>): Boolean {
        return Authentication::class.java.isAssignableFrom(auth)
    }
}

Remember to update your Security Config to register your new provider:

Java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomAuthenticationManager authenticationManager;

    public SecurityConfig(CustomAuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(requests -> requests.anyRequest().authenticated());
        http.httpBasic(Customizer.withDefaults());
        http.authenticationProvider(authProvider);
        return http.build();
    }
}
Kotlin
@Configuration
@EnableWebSecurity
class SecurityConfig(
    val authProvider: CustomAuthenticationProvider,
) {
    @Bean
    fun configure(http: HttpSecurity): SecurityFilterChain {
        http.authorizeHttpRequests { requests -> requests.anyRequest().authenticated() }
        http.httpBasic(Customizer.withDefaults())
        http.authenticationProvider(authProvider)
        return http.build()
    }
}

Testing and verification

After you configure the AuthenticationManager, you can check its operation. First, let's add a simple controller that will return "Hello, World!" when you request the "/" endpoint:

Java
@RestController
public class IndexController {

    @GetMapping("/")
    public String index() {
        return "Hello, World!";
    }
}
Kotlin
@RestController
class IndexController {
    @GetMapping("/")
    fun index() = "Hello, World!"
}

Now if you open the browser and try to make a request to this endpoint, you will see that your browser asks for credentials:

Credentials window

If you enter incorrect login details, the page will reload, and the window will reappear, preventing you access to the page's content. When you input the credentials you specified earlier (admin as the username and password as the password), the window will disappear, and you'll gain access to the requested resource:

Hello World

Conclusion

In short, the AuthenticationManager in the Spring Security framework plays a critical role in securing applications. It manages authentication processes effectively, serving as a central point for all authentication requests and facilitating a unified, organized approach to security.

9 learners liked this piece of theory. 2 didn't like it. What about you?
Report a typo