When we fail to log in, we may write the wrong username or the wrong password, but for security reasons, the server usually does not explicitly indicate whether the username or the password is wrong, but only gives a vague username or password error.

However, for many programmers, they may not be aware of the “unspoken rules” that may give the user a clear indication of whether the user name is wrong or the password is wrong.

To avoid this, Spring Security hides the username does not exist exception by wrapping it so that when developing, developers only get BadCredentialsException which indicates both that the username does not exist and that the user has entered the wrong password. To ensure that our system is secure enough.

However, for various reasons, sometimes we want to get separate exceptions for non-existent users and incorrect passwords, which requires some simple customization of Spring Security.

1. Source code analysis

First we need to find out why and where the problem occurs.

In Spring Security, there are many classes responsible for user authentication, and the key class involved is AbstractUserDetailsAuthenticationProvider.

This class will be responsible for the authentication of the username and password, specifically in the authenticate method, which is particularly long, so I will only list the code relevant to this article.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        try {
            user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (UsernameNotFoundException ex) {
            if (!this.hideUserNotFoundExceptions) {
                throw ex;
            }
            throw new BadCredentialsException(this.messages
                    .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
    }
}

The retrieveUser method is to find the user based on the username entered by the user, if not found, it will throw a UsernameNotFoundException, after this exception is caught, it will first determine whether to hide this exception, if not, the original exception will be thrown unchanged, if it needs to be hidden, it will throw a BadCredentialsException is literally an exception for password input errors.

So the heart of the problem becomes the hideUserNotFoundExceptions variable.

This is a Boolean type property that defaults to true, and AbstractUserDetailsAuthenticationProvider also provides a set method for this property.

1
2
3
public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions) {
    this.hideUserNotFoundExceptions = hideUserNotFoundExceptions;
}

It looks like it’s not too hard to modify the hideUserNotFoundExceptions property! Just find the instance of AbstractUserDetailsAuthenticationProvider and call the corresponding set method.

Now the heart of the matter becomes where to get the instance of AbstractUserDetailsAuthenticationProvider?

As you know from the name, AbstractUserDetailsAuthenticationProvider is an abstract class, so its instance is actually an instance of its subclass, who is the subclass? Of course, it’s DaoAuthenticationProvider, which is responsible for user password verification.

Remember this knowledge first, we will use it later.

2. Login process

In order to understand this, we also need to understand a general authentication process for Spring Security, which is also very important.

First of all, you know that authentication in Spring Security is mainly done by AuthenticationManager, which is an interface whose implementation class is ProviderManager. In short, it is the ProviderManager#authenticate method that is specifically responsible for authentication in Spring Security.

But the verification is not done directly by the ProviderManager, the ProviderManager manages a number of AuthenticationProvider, the ProviderManager will call the AuthenticationProvider it manages to complete the verification work as follows Figure.

Login process

On the other hand, ProviderManager is divided into global and local.

When we log in, the local ProviderManager will first come out to verify the user name and password, if the verification is successful, then the user is logged in successfully, if the verification fails, then the parent of the local ProviderManager, that is, the global ProviderManager, will be called to complete the verification. If the global ProviderManager is successful, it means the user is successfully logged in, if the global ProviderManager fails, it means the user failed to log in, as follows.

ProviderManager

OK, with the above knowledge base, let’s analyze what we should do if we want to throw a UsernameNotFoundException.

3. Idea Analysis

First of all, our user verification is done in a local ProviderManager. The local ProviderManager manages a number of AuthenticationProviders, which may contain the DaoAuthenticationProvider we need. Do we need to call the setHideUserNotFoundExceptions method of the DaoAuthenticationProvider here to finish modifying the properties?

No need! Why?

Because when a user logs in, the first thing to do is to check in the local ProviderManager, and if the check is successful, of course, it is best; if the check fails, it will not immediately throw an exception, but will continue to check in the global ProviderManager, so that even if we have thrown a UsernameNotFoundException is useless even if we throw it in the local ProviderManager, because ultimately the decision to throw this exception lies in the global ProviderManager (if the DaoAuthenticationProvider managed by the global ProviderManager does not do any special processing, then the local UsernameNotFoundException thrown in the ProviderManager will eventually be hidden).

So, all we have to do is to get the global ProviderManager, and then get the DaoAuthenticationProvider managed by the global ProviderManager, and then call its setHideUserNotFoundExceptions method to modify the corresponding property values.

Once you understand the principle, the code is simple.

4. Specific Practices

The global ProviderManager is modified in the WebSecurityConfigurerAdapter#configure(AuthenticationManagerBuilder) class, and the AuthenticationManagerBuilder configured here is eventually is used to generate the global ProviderManager, so our configuration is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
        InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
        userDetailsService.createUser(User.withUsername("javaboy").password("{noop}123").roles("admin").build());
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        auth.authenticationProvider(daoAuthenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .failureHandler((request, response, exception) -> System.out.println(exception))
                .permitAll();

    }

}

The code here is simple.

  • Create a DaoAuthenticationProvider object.
  • Call the DaoAuthenticationProvider object’s setHideUserNotFoundExceptions method of the DaoAuthenticationProvider object and modify the values of the corresponding properties.
  • Configure the user data source for the DaoAuthenticationProvider.
  • Set the DaoAuthenticationProvider to the auth object, auth will be used to generate the global ProviderManager.
  • In another configure method, we’ll just configure the login callback and print an exception message when the login fails.

Next, start the project to test it. Enter an incorrect username and you can see that the IDEA console prints the following message.

1
org.springframework.security.core.userdetails.UsernameNotFoundException:sentinel

UsernameNotFoundException has been thrown.

Reference http://www.enmalvi.com/2021/10/29/springsecurity-badcredentialsexception/