1

I'm facing an issue with a SpringBoot application. My ICS reports high response times on this API but my code metrics says it's all fine, for example a query which is sent to my API takes only 50ms to run the "business" code but the effective response takes more than 500ms !

The internal logs of my JVM says that on each request, I have a "UsernameNotFoundExcption" even if credentials are correct and API works fine. That's why I think my problem comes from SpringSecurity layer but I can't identify the cause.

My entry point:

@Component
public class BasicAuthenticationPoint extends BasicAuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authEx)
            throws IOException, ServletException {
        response.addHeader("WWW-Authenticate", "Basic realm=" + getRealmName());
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setCharacterEncoding("utf-8");
        PrintWriter writer = response.getWriter();
        writer.println("HTTP Status 401 - " + authEx.getMessage());
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        setRealmName("MYAPI");
        super.afterPropertiesSet();
    }
}

And my adapter:

@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private BasicAuthenticationPoint basicAuthenticationPoint;

    @Bean
    public BCryptPasswordEncoder encoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        HttpSecurity httpSec = http.csrf().disable();
        httpSec = httpSec.authorizeRequests()
                .antMatchers("/my-business-resources/**").hasRole("USER")
                .antMatchers("/actuator/**").hasRole("ADMIN")
                .and();
        httpSec.httpBasic().authenticationEntryPoint(basicAuthenticationPoint).and()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        // Load users file
        Resource confFile = resourceLoader.getResource("classpath:users.list");
        
        // Returns users from the configuration file <username, password (BCrypted), admin (boolean)>
        List<ApiUser> users = ApiUtils.getUsersFromFile(confFile);
        
        for(ApiUser u : users){
            // Add the username/password to the in-memory authentication manager
            if (u.admin)
                auth.inMemoryAuthentication().withUser(u.username).password(u.password).roles("USER", "ADMIN");
            else
                auth.inMemoryAuthentication().withUser(u.username).password(u.password).roles("USER");

        }
    }
}

Did I miss something?

PS: My Spring Boot application is packaged as a WAR and executed inside a Tomcat server for standardization purposes.

EDIT:

Here's the full UsernameNotFound stacktrace (ICS formatted):

Exception Details
Type:   UsernameNotFoundException
Exception Class:    org.springframework.security.core.userdetails.UsernameNotFoundException
API:    Exception
Thread Name:    https-openssl-apr-9343-exec-10 <522634598>

Exception StackTrace
Method  Class   Line    File Name
loadUserByUsername  org.springframework.security.provisioning.InMemoryUserDetailsManager    146 <unknown>
retrieveUser    org.springframework.security.authentication.dao.DaoAuthenticationProvider   104 <unknown>
authenticate    org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider   144 <unknown>
authenticate    org.springframework.security.authentication.ProviderManager 174 <unknown>
authenticate    org.springframework.security.authentication.ProviderManager 199 <unknown>
doFilterInternal    org.springframework.security.web.authentication.www.BasicAuthenticationFilter   180 <unknown>
doFilter    org.springframework.web.filter.OncePerRequestFilter 107 <unknown>
doFilter    org.springframework.security.web.FilterChainProxy$VirtualFilterChain    334 <unknown>
doFilter    org.springframework.security.web.authentication.logout.LogoutFilter 116 <unknown>
doFilter    org.springframework.security.web.FilterChainProxy$VirtualFilterChain    334 <unknown>
doFilterInternal    org.springframework.security.web.header.HeaderWriterFilter  66  <unknown>
doFilter    org.springframework.web.filter.OncePerRequestFilter 107 <unknown>
doFilter    org.springframework.security.web.FilterChainProxy$VirtualFilterChain    334 <unknown>
doFilter    org.springframework.security.web.context.SecurityContextPersistenceFilter   105 <unknown>
doFilter    org.springframework.security.web.FilterChainProxy$VirtualFilterChain    334 <unknown>
doFilterInternal    org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter 56  <unknown>
doFilter    org.springframework.web.filter.OncePerRequestFilter 107 <unknown>
doFilter    org.springframework.security.web.FilterChainProxy$VirtualFilterChain    334 <unknown>
doFilterInternal    org.springframework.security.web.FilterChainProxy   215 <unknown>
doFilter    org.springframework.security.web.FilterChainProxy   178 <unknown>
invokeDelegate  org.springframework.web.filter.DelegatingFilterProxy    357 <unknown>
doFilter    org.springframework.web.filter.DelegatingFilterProxy    270 <unknown>
internalDoFilter    org.apache.catalina.core.ApplicationFilterChain 193 <unknown>
doFilter    org.apache.catalina.core.ApplicationFilterChain 166 <unknown>
doFilter    org.springframework.boot.web.servlet.support.ErrorPageFilter    130 <unknown>
access$000  org.springframework.boot.web.servlet.support.ErrorPageFilter    66  <unknown>
doFilterInternal    org.springframework.boot.web.servlet.support.ErrorPageFilter$1  105 <unknown>
doFilter    org.springframework.web.filter.OncePerRequestFilter 107 <unknown>
doFilter    org.springframework.boot.web.servlet.support.ErrorPageFilter    123 <unknown>
internalDoFilter    org.apache.catalina.core.ApplicationFilterChain 193 <unknown>
doFilter    org.apache.catalina.core.ApplicationFilterChain 166 <unknown>
filterAndRecordMetrics  org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter    155 <unknown>
filterAndRecordMetrics  org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter    123 <unknown>
doFilterInternal    org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter    108 <unknown>
doFilter    org.springframework.web.filter.OncePerRequestFilter 107 <unknown>
internalDoFilter    org.apache.catalina.core.ApplicationFilterChain 193 <unknown>
doFilter    org.apache.catalina.core.ApplicationFilterChain 166 <unknown>
doFilterInternal    org.springframework.web.filter.CharacterEncodingFilter  200 <unknown>
doFilter    org.springframework.web.filter.OncePerRequestFilter 107 <unknown>
internalDoFilter    org.apache.catalina.core.ApplicationFilterChain 193 <unknown>
Jason Aller
  • 3,541
  • 28
  • 38
  • 38
Dynamite
  • 389
  • 5
  • 17
  • The performance penalty is because it tries to authenticate with each request and fails. API works fine probably because Spring Security actually doesn't protect your endpoints. –  Dec 03 '18 at 14:36
  • @EugenCovaci So can I disable Spring internal filters and use only mine ? – Dynamite Dec 03 '18 at 14:39
  • No, as a first step, try to log the `List users`, maybe in production the list in not as expected. –  Dec 03 '18 at 14:42
  • @EugenCovaci It is correct because API authentication works fine (I mean credentials are effectively required and wrong user/psw ends in 401) – Dynamite Dec 03 '18 at 14:50
  • Hi! Can you show the entire `UsernameNotFoundExcption` stacktrace error? – Dherik Dec 03 '18 at 14:52
  • @Dherik Have fun ;) – Dynamite Dec 03 '18 at 14:58
  • Please run it in debug mode. –  Dec 03 '18 at 15:18

3 Answers3

1

I believe the reason that it is taking 500ms is due to password encoding. BCrypt is an encoding mechanism that is designed to make authentication take longer in order to mitigate brute force attacks.

Typically, this performance issue is solved by exchanging long-lived credentials (like passwords) with short-lived ones like tokens. OAuth is a framework that attempts to standardize this practice. Spring Security 5.1 introduces support for protecting APIs with OAuth 2.0. This won't immediately fix your performance issue since it may require introducing more infrastructure, but it is probably a better security design choice in the long term.

As an alternative, you could use a weaker encoder or possibly fewer rounds of BCrypt, but note that both of these are a performance-security tradeoff. You'd change out your BCryptPasswordEncoder for something else, e.g.:

@Bean
public PasswordEncoder encoder() {
    // ...
}

EDIT: Glad you found the issue! Tangentially, I'd recommend cleaning up the way that you are describing your authentication manager. It can be replaced with a UserDetailsService:

@Bean
@Override
public UserDetailsService userDetailsService() {
    Resource confFile = resourceLoader.getResource("classpath:users.list");
    Collection<UserDetails> users = ApiUtils.getUsersFromFile(confFile)
        .stream().map(this::toUserDetails)
        .collect(Collectors.toList());
    return new InMemoryUserDetailsManager(users);
}

private UserDetails toUserDetails(ApiUser apiUser) {
    UserBuilder builder = User.builder()
        .username(apiUser.username)
        .password(apiUser.password);
    return apiUser.admin ?
        builder.roles("USER", "ADMIN").build() :
        builder.roles("USER").build();
}

This is nice because it has a smaller configuration footprint (an authentication manager is a "bigger" thing than a user details service). If you are just wanting to supply users, then it is more typical to override the UserDetailsService instead of the AuthenticationManager.

jzheaux
  • 7,042
  • 3
  • 22
  • 36
  • I had this idea and tried with plain text passwords but it wasn't faster (or only few milliseconds maybe), moreover I'm using BCrypted password with the lower number of rounds available. – Dynamite Dec 04 '18 at 09:56
  • @Dynamite just to clarify, when you say "with plain text passwords" do you mean that you replaced the `BCryptPasswordEncoder` bean with `NoOpPasswordEncoder`? – jzheaux Dec 04 '18 at 17:46
1

Okay, after a lot of debug, I found the problem !

The issue is in the code I use to fill the in memory authenticator:

for(ApiUser u : users){
    // Add the username/password to the in-memory authentication manager
    if (u.admin)
        auth.inMemoryAuthentication().withUser(u.username).password(u.password).roles("USER", "ADMIN");
    else
        auth.inMemoryAuthentication().withUser(u.username).password(u.password).roles("USER");
}

The fact of calling auth.inMemoryAuthentication() many times creates a new InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> on each time (Spring code), and so we have as many different AuthenticationManager as users (one user per each) so the authentication process is performed multiple times for each requests.

Here is how I fixed the bug:

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    // Load users file
    Resource confFile = resourceLoader.getResource("classpath:users.list");

    // Returns users from the configuration file <username, password (BCrypted), admin (boolean)>
    List<ApiUser> users = ApiUtils.getUsersFromFile(confFile);

    @SuppressWarnings("rawtypes")
    UserDetailsBuilder udb = null;

    for(ApiUser u : users){
        // Add the username/password to the in-memory authentication manager
        if (udb == null)
            udb = auth.inMemoryAuthentication().withUser(u.username).password(u.password);
        else
            udb = udb.and().withUser(u.username).password(u.password);

        if (u.admin)
            udb.roles("USER", "ADMIN");
        else
            udb.roles("USER");
    }
}

Now my average response time is 80ms

Dynamite
  • 389
  • 5
  • 17
0

You are Performing security checks on your resources and other files as well. You should exclude them to be authenticated at all.

Have a look here: Spring Security: how to exclude certain resources?

comrad
  • 105
  • 7
  • I updated my question with the antMatchers shown, I don't think I'm hidding any "public" ressources. What I want is having every ressources to be public except those under "/my-business-ressources" and "/actuator". Am I wrong ? – Dynamite Dec 03 '18 at 15:23
  • Spring Security works with a filter which is probably your root Directory for you web appplication. Because of that every url of your web application will be handled by Spring Security. Do you have any Images or css files? Those are probably the reason for the UsernameNotFoundExcption because your browser will not send credentials for these. Common solution for this is to exclude some URLs from being excluded by Spring Security at all. – comrad Dec 03 '18 at 15:29
  • This is a database access API, I don't have any resources beside those. – Dynamite Dec 03 '18 at 16:35
  • @comrad since the link you included is for XML and the OP is using Java config, it might be helpful to show a quick snippet here in this answer on what the Java config would look like. – jzheaux Dec 03 '18 at 16:38