How to make Spring Security "mind your own business"

Posted by scept1c on Mon, 03 Jan 2022 21:29:09 +0100

Remember two ways to let Spring Security "mind your own business".

Problems encountered

An application provides an external Rest interface. The access authentication of the interface is controlled by Spring Security OAuth2, and the token form is JWT. For some reasons, the interface with a specific path prefix (assumed to be / custom /) needs to use another user-defined authentication method. The token is an irregular random string. The tokens of both authentication methods are passed in Headers in the form of authorization: Bear XXX.

Therefore, when the external requests the interface of this application, the situation is as follows:

At this time, the problem arises.

I configure Spring Security through WebSecurityConfigurerAdapter to directly release the request with / custom / prefix:

httpSecurity.authorizeRequests().regexMatchers("^(?!/custom/).*$").permitAll();

However, the interface requesting the / custom / prefix is still blocked, and the following error is reported:

{
  "error": "invalid_token",
  "error_description": "Cannot convert access token to JSON"
}

Analyze problems

From the error prompt, you can first eliminate the suspicion of CustomWebFilter by checking. The token of the custom authentication method is not in JSON format. Naturally, it is not in JSON format. Try to convert it into JSON.

It is speculated that the problem lies in Spring Security's "meddling" in intercepting requests that should not be intercepted.

After some search oriented programming and source code debugging, it is found that the location where the above error message is thrown is jwtaccesstokenconverter In the decode method:

protected Map<String, Object> decode(String token) {
    try {
        // The following line throws an exception
        Jwt jwt = JwtHelper.decodeAndVerify(token, verifier);
        // ... some code here
    }
    catch (Exception e) {
        throw new InvalidTokenException("Cannot convert access token to JSON", e);
    }
}

The call stack is as follows:

As can be seen from the context of the call (highlight the line), the execution logic is in a Filter called OAuth2AuthenticationProcessingFilter. It will try to extract the bear token from the request and then do some processing (JWT conversion and verification here). This Filter is initialized in ResourceServerSecurityConfigurer.configure. Our application is also a Spring Security OAuth2 Resource Server. It can be seen from the class name that this Filter is configured.

solve the problem

After finding the problem, through their own thinking and discussion among colleagues, two feasible solutions are obtained.

Scheme 1: let specific requests skip oauthauthenticationprocessingfilter

The idea of this scheme is through AOP in oauthauthenticationprocessingfilter Make a judgment before the dofilter method is executed

  1. If the request path starts with / custom /, skip the Filter and continue to execute later;
  2. If the request path does not start with / custom /, it is executed normally.

Key code diagram:

@Aspect
@Component
public class AuthorizationHeaderAspect {
    @Pointcut("execution(* org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter.doFilter(..))")
    public void securityOauth2DoFilter() {}

    @Around("securityOauth2DoFilter()")
    public void skipNotCustom(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        if (args == null || args.length != 3 || !(args[0] instanceof HttpServletRequest && args[1] instanceof javax.servlet.ServletResponse && args[2] instanceof FilterChain)) {
            joinPoint.proceed();
            return;
        }
        HttpServletRequest request = (HttpServletRequest) args[0];
        if (request.getRequestURI().startsWith("/custom/")) {
            joinPoint.proceed();
        } else {
            ((FilterChain) args[2]).doFilter((ServletRequest) args[0], (ServletResponse) args[1]);
        }
    }
}

Scheme 2: adjust Filter order

If the request can reach our custom Filter first, and the request path starts with / custom /, the user-defined token verification and other logic can be processed, Then remove the Authorization Header (in OAuth2AuthenticationProcessingFilter.doFilter, if the Bearer Token cannot be obtained, no exception will be thrown). Other requests can be released directly, which is also an idea to achieve the goal.

However, the status quo is that custom filters are executed after oauthauthenticationprocessingfilter by default. How to adjust their execution order?

The registration place of oauthauthenticationprocessingfilter found earlier is resourceserversecurityconfigurer In the configure method, we can see that Filter is added through the following writing method:

@Override
public void configure(HttpSecurity http) throws Exception {
    // ... some code here
    http
        .authorizeRequests().expressionHandler(expressionHandler)
    .and()
        .addFilterBefore(resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class)
        .exceptionHandling()
            .accessDeniedHandler(accessDeniedHandler)
            .authenticationEntryPoint(authenticationEntryPoint);
}

The core method is HttpSecurity Addfilterbefore, speaking of HttpSecurity, we are impressed... The input parameter of the request release is configured through WebSecurityConfigurerAdapter. Can you register the custom Filter before oauthauthenticationprocessingfilter at that time?

We will modify the code at the release rule configured above as follows:

// ...
httpSecurity.authorizeRequests().registry.regexMatchers("^(?!/custom/).*$").permitAll()
        .and()
        .addFilterAfter(new CustomWebFilter(), X509AuthenticationFilter.class);
// ...

Note: the CustomWebFilter is changed to new and added to the Security Filter Chain manually. It will not be automatically injected into other filter chains.

Why add a custom Filter to x509authenticationfilter After class? You can refer to the preset Filter order in the filtercomprator of the spring security config package to make a decision. From the previous code, we can see that oauthauthenticationprocessingfilter is added to abstractpreauthenticatedprocessingfilter Class, and in the preset order of FilterComparator, x509authenticationfilter Class is in abstractpreauthenticatedprocessingfilter Class, this is enough to ensure that the custom Filter is before oauthauthenticationprocessingfilter.

After the above modifications, the custom Filter is already in the expected position. In this Filter, we can do necessary processing for the request path starting with / custom /, and then clear the Authorization Header. The key code is shown as follows:

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) servletRequest;
    if (request.getServletPath().startsWith("/custom/")) {
        // do something here
        // ...
        final String authorizationHeader = "Authorization";
        HttpServletRequestWrapper requestWrapper = new HttpServletRequestWrapper((HttpServletRequest) servletRequest) {
            @Override
            public String getHeader(String name) {
                if (authorizationHeader.equalsIgnoreCase(name)) {
                    return null;
                }
                return super.getHeader(name);
            }

            @Override
            public Enumeration<String> getHeaders(String name) {
                if (authorizationHeader.equalsIgnoreCase(name)) {
                    return new Vector<String>().elements();
                }
                return super.getHeaders(name);
            }
        };
        filterChain.doFilter(requestWrapper, servletResponse);
    } else {
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

Summary

After trying, both schemes can meet the needs. Scheme I is finally used in the project. I believe there are other ideas to solve the problem.

After this process, it also exposed the problem of insufficient understanding of Spring Security. In the future, we need to take time to do some more in-depth study.

reference resources

Topics: Java Spring spring-security