Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to speed up SpringBoot 2.0 basic authentication?

I recently updated a Spring Boot application from v1.5 to v2.0.3. It's an application with methods exposed as REST endpoints and secured by Basic HTTP authentication. The usernames and passwords are hardcoded in a properties file loaded by the application.

Since the update, the response time increased by almost 200ms, and 98% of the time processing a request is spent in BasicAuthenticationFilter.doFilter().

new relic transaction details

More specifically, time is spent encoding the password in the request to compare it with the password provided by configuration.

visualvm details

Here's an extract of the SecurityConfig class:

@EnableWebSecurity
@PropertySource("classpath:auth/auth.properties")
public class SecurityConfig extends WebSecurityConfigurerAdapter {

     @Value("${user.name}")
     private String userName;
     @Value("${user.password}")
     private String userPassword;
     @Value("${user.roles}")
     private String[] userRoles;

     @Override
     protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
        PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
        UserDetails user = User.withUsername(userName).password(encoder.encode(userPassword)).roles(userRoles).build();
        auth.inMemoryAuthentication().withUser(user);
    }

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        //make sure that the basic authentication header is always required
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        //set the correct authentication entry point to make sure the 401 response is in JSON format
        http.exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPoint());

        //enable http basic authentication
        http.httpBasic();

        //make sure authentication is required for all requests except /actuator/info
        http.authorizeRequests().antMatchers("/actuator/info").permitAll().anyRequest().authenticated();
        http.authorizeRequests().antMatchers("/actuator/**").hasAnyRole("MONITOR", "ACTUATOR");

        //disable the form login since this is a REST API
        http.formLogin().disable();

        //disable csrf since this is a REST API
        http.csrf().disable();
    }
}

To verify that it was due to the Spring Boot update, I locally reverted the changes and ran some tests. The response time was divided by 4.

I've tried a few things already but none of them improved the response time:

  • Disable oAuth (and cors) login as it seems to be slow as well https://github.com/spring-projects/spring-security-oauth/issues/943
  • Provide a UserDetailsService bean instead of configuring the AuthenticationManagerBuilder https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-Security-2.0#changing-the-username-and-password

Can I do anything to speed up the authentication filter?

like image 411
Philippe A Avatar asked Aug 27 '18 13:08

Philippe A


2 Answers

bcrypt is an intentionally slow hashing function. While this slowness sounds paradoxical when it comes to password hashing it isn't because both the good guys and the bad are slowed down. These days most password attacks are some variant of a brute force dictionary attack. This means that an attacker will try many, many candidate passwords by hashing them just like the good guys do. If there is a match, the password has been cracked.

However, if the bad guy is slowed down per try that is amplified when millions and millions of attempts are made, often to the point of thwarting the attack. Yet the good guys probably won't notice on a single attempt when they try to log in.

So basically the extra 200ms processing time inside the brypt encoder is intentional and IS PART OF THE SECURITY By speeding it up (like using cache) you decrease the security level of your application.

--- edit:

Btw. There is still a quick AND secure way to evaluate the password-match: Use some cache-service (like in other answers here), but store only the matched values in the cache!!! This way if a user provides a valid password, it will be evaluated slowly only once - at the very first time - but all of his subsequent logins will be quick. But if an attacker tries using brute-force, then all of his attempts will take 200ms.

like image 104
Selindek Avatar answered Oct 21 '22 06:10

Selindek


I had a similar problem, and fixed it by wrapping caching around the password encoder (which could be used in the previous answer). Assuming the same user ids are used frequently this should solve the problem. It uses the Caffeine caching library.

private static class CachingPasswordEncoder implements PasswordEncoder {
    private final PasswordEncoder encoder = new BCryptPasswordEncoder();
    private final Cache<CharSequence, String> encodeCache = Caffeine.newBuilder().build();
    private final Cache<PasswordMatchKey, Boolean> matchCache = Caffeine.newBuilder().build();

    @Override
    public String encode(final CharSequence rawPassword) {
        return encodeCache.get(rawPassword, s -> encoder.encode(rawPassword));
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        //noinspection ConstantConditions
        return matchCache.get(new PasswordMatchKey(rawPassword, encodedPassword),
                k -> encoder.matches(rawPassword, encodedPassword));
    }
}

private static class PasswordMatchKey {
    private final CharSequence rawPassword;
    private final String encodedPassword;

    public PasswordMatchKey(CharSequence rawPassword, String encodedPassword) {
        this.rawPassword = rawPassword;
        this.encodedPassword = encodedPassword;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PasswordMatchKey that = (PasswordMatchKey) o;
        return Objects.equals(rawPassword, that.rawPassword) &&
                Objects.equals(encodedPassword, that.encodedPassword);
    }

    @Override
    public int hashCode() {
        return Objects.hash(rawPassword, encodedPassword);
    }
}
like image 24
BarrySW19 Avatar answered Oct 21 '22 05:10

BarrySW19