10

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:

Can I do anything to speed up the authentication filter?

Philippe A
  • 1,252
  • 9
  • 22
  • I suggest you to use some profiling tools (e.g. VisualVM) to discover where the code spends majority of the time. – Pavel Horal Aug 27 '18 at 13:42
  • Yes I intend to profile app although it seems clear from the New Relic transaction details (cf screenshot) that the time is spent in the filter. – Philippe A Aug 27 '18 at 13:57
  • Lot of stuff happens during authentication processing. Knowing "it happens inside BasicAuthenticationFilter" is a good start, but still very general intel. – Pavel Horal Aug 27 '18 at 14:06
  • It might be some faulty authentication success event listener. – Pavel Horal Aug 27 '18 at 14:13
  • Thanks, I added more details. Tomorrow I'll check how it was on the previous version, see if it wasn't using BCrypt for example. – Philippe A Aug 27 '18 at 14:44
  • For better performance, maybe you can try to implement your own web filter to check the basic authentication string in request header. – LHCHIN Aug 28 '18 at 01:53

5 Answers5

6

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.

Selindek
  • 3,269
  • 1
  • 18
  • 25
1

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);
    }
}
BarrySW19
  • 3,759
  • 12
  • 26
0

Indeed one solution is to use another password encoder, for ex in the SecurityConfig class:

@Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService())
        .passwordEncoder(passwordEncoder());
}

@Bean
public UserDetailsService userDetailsService() {
    PasswordEncoder encoder = passwordEncoder();
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    UserDetails user = User.withUsername(userName).password(encoder.encode(password)).roles(userRoles).build();
    manager.createUser(user);
    return manager;
}

private PasswordEncoder passwordEncoder() {
    // return a fast password encoder
}

Now, which encryption algorithm to use is another question. Although bcrypt seems to be a safe bet, I wonder if it's necessary considering the password are stored in memory and thus difficult to access.

See also

Philippe A
  • 1,252
  • 9
  • 22
0

Encrypted password encoders bring a latency and CPU load on your services. For cases like fixed password/loaded from prop file etc. you may reduce security level to increase your performance.

You may update your config method like this:

@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception
{
    // we accept low level security for password encoding for better performance
    PasswordEncoder encoder = NoOpPasswordEncoder.getInstance();

    InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> configurer = auth.inMemoryAuthentication();
    configurer.passwordEncoder(encoder);

    UserDetails user = User.withUsername(userName).password(encoder.encode(userPassword)).roles(userRoles).build();
    configurer.withUser(user);
}

It'll use deprecated NoOpPasswordEncoder directly and won't load any encryption based encoders. Don't be afraid about the deprecation warning since it also states that they don't plan to remove the class, they're just warning you about the security.

Since you're loading passwords from the prop file, you're already not secure :)

ahmkara
  • 559
  • 6
  • 6
0

Spring Security default behaviour changed

Earlier to 5, default behaviour was just plain old base64, Now its bcrypt. Change it to old behaviour by using NoOpPasswordEncoder

suresh
  • 2,365
  • 1
  • 26
  • 36