HOAB

History of a bug

Tomcat, NIO, Hanging et CLOSE_WAIT

Rédigé par gorki Aucun commentaire

Problem :

We are testing a springboot application in AWS with ELB in front.

After a while of load-testing, the application was hanging :

  • HTTP 504 error code from Jmeter client
  • HTTP 502 if we raise ELB timeout
  • Once logged on the server :
    • telnet localhost 8080 was OK
    • sending GET / on this socket was not responding
    • plenty of CLOSE_WAIT socket
    • wget was also hanging (normal)
    • connection was established during wget hang
    • nothing in the log

 

Solution :

 

I initially think about the keepAlive timeout and pool of tomcat but

  1. SpringBoot copy the connectionTimeout parameter to keepAliveTimeout
  2. new socket is accepted and established
  3. CLOSE_WAIT wasn't shutdown after hour

Doing the test many times, I finally so a classical "Too many open files" in the log. That's why I could not see more log during the hang.

So we change the nproc and nofile in /etc/security/limits.conf

And taadaaaa ! Nothing change in :

cat /proc/<$PID>/limits

Thanks to blogs over the world like this one :

  • the service is start with systemd
  • to override ressources limits with systemd :
[Service]
...
LimitNOFILE=500000
LimitNPROC=500000

At last but not least, the value of Tomcat NIO socket queue is around 10000 + other files + other process... choose wisely your limit

SpringBoot 2, OAUTH 2 and client_credentials in POST parameters

Rédigé par gorki Aucun commentaire

Problem :

Edited : due to a bug, see also (https://www.hoab.fr/springboot-2-oauth-2-and-tokenstore)

I try to implements client credentials OAUTH flow for a server which is resource and authentication server. (Goody summary here : https://www.slideshare.net/halyph/oauth2-and-spring-security)

There is plenty of posts on how to make an oauth 2 authentication server, (like authentication code flow  less with client_credentials mode...

And in these examples :

But if I easily found how to perform this kind of request : POST + BasicAuth(client_id/client_secret), I wasn't able to do :

POST + optional BasicAuth + client_id/client_secret in post parameters

Solution :

I activate DEBUG on spring security and see that filter chains do not have UsernamePasswordAuthenticationFilter, I plan to add the filter in websecurity as in some examples but it was not working.

After a lot of unsuccessful tries, I finally understand that I have to add a kind of UsernamePasswordAuthenticationFilter on OAUTH2 filter chain and not on the others (yes, there is multiple security filter chain....)

Going back to origins, I check AuthorizationServerConfigurerAdapter override method and ... eurêka !

As simple as add a

ClientCredentialsTokenEndpointFilter

with

security.allowFormAuthenticationForClients()

method... So simple, so hard to find.

But my AuthorizationServer is now :

Due to a bug, the code is now in (https://www.hoab.fr/springboot-2-oauth-2-and-tokenstore)

And a simple (for now) client detail service :

package com.example;

import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;

import java.util.Arrays;

public class MyClientDetailsService implements ClientDetailsService {

    private static final String CLIENT_CREDENTIALS = "client_credentials";
    private static final String REFRESH_TOKEN = "refresh_token";
    private static final String SCOPE_READ = "read";
    private static final String SCOPE_WRITE = "write";
    private static final String TRUST = "trust";
    private static final int VALID_FOREVER = -1;


    private static final String CLIENT_ID = "my-client";
    // encoding method prefix is required for DelegatingPasswordEncoder which is default since Spring Security 5.0.0.RC1
    // you can use one of bcrypt/noop/pbkdf2/scrypt/sha256
    // you can change default behaviour by providing a bean with the encoder you want
    // more: https://spring.io/blog/2017/11/01/spring-security-5-0-0-rc1-released#password-encoding
    static final String CLIENT_SECRET = "{noop}my-secret";


    @Override
    public ClientDetails loadClientByClientId(String s) throws ClientRegistrationException {
        if (s.equals(CLIENT_ID)) {
            BaseClientDetails client = new BaseClientDetails();
            client.setClientId(s);
            client.setClientSecret(CLIENT_SECRET);
            client.setAuthorizedGrantTypes(Arrays.asList(CLIENT_CREDENTIALS, REFRESH_TOKEN));
            client.setScope(Arrays.asList(SCOPE_READ, SCOPE_WRITE, TRUST));
            client.setAccessTokenValiditySeconds(VALID_FOREVER);
            client.setRefreshTokenValiditySeconds(VALID_FOREVER);
            return client;
        }
        return null;
    }


}

And my resource server :

Due to a bug, the code is now in (https://www.hoab.fr/springboot-2-oauth-2-and-tokenstore)

The CORS filter from (https://www.hoab.fr/springboot-2-oauth-2-options-and-cors) :

package com.example;

import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SimpleCorsFilter implements Filter {

    public SimpleCorsFilter() {
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        HttpServletRequest request = (HttpServletRequest) req;
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with, authorization, content-type");

        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            chain.doFilter(req, res);
        }
    }

    @Override
    public void init(FilterConfig filterConfig) {
    }

    @Override
    public void destroy() {
    }
}

And a protected resource :

package com.example;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/api/metrics")
@RestController
public class Metrics {

    @GetMapping
    String getToken() {
        return "OK";
    }
}

 

 

 

 

SpringBoot 2, OAUTH 2, OPTIONS and CORS

Rédigé par gorki Aucun commentaire

Problem :

I have a standalone application Angular running on localhost:4200 and a standalone SpringBoot2 Oauth running on 8080.

OAUTH is configured with "client_credentials".

As indicate by many articles, the OPTIONS preflight request is issued by browsers to request CORS configuration supported by servers.

I added the CORS filters thanks to this post, normal Webrequest CORS configuration is not working for OAUTH as this one is not handle by Spring MVC.

But still no POST request after the preflight OPTIONS request...

Solution :

I was checking my network requests in the browser console... and the solution was simply in the console... Yeah. Too early this morning.

The error was explicity written : "need to enable content-type headers in the Access-Control-Allow-Headers".

The preflight request say to the browser what kind of request can be emit, if the response of the OPTIONS does not match the original request, this one is not sent.

The preflight request has no authentication headers and must return 200.

Here is the filter code :

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SimpleCorsFilter implements Filter {

    public SimpleCorsFilter() {
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        HttpServletRequest request = (HttpServletRequest) req;
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with, authorization, content-type");

        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            chain.doFilter(req, res);
        }
    }

    @Override
    public void init(FilterConfig filterConfig) {
    }

    @Override
    public void destroy() {
    }
}

 

 

 

 

Fil RSS des articles