Search code examples
spring-bootpkixspring-resource-serverspring-security-6

Calling any private (non-public) REST requests causes PKIX path building failed problem on Spring Security 6 (OAuth2ResourceServer)


I recently migrated to Spring Boot 3, Java 17 & Spring Security 6. I have a working authorization server already deployed for testing and prod environments, so there is no need to implement it from scratch inside my project. The only thing left is to properly implement resource server. Before Spring Boot 3 I used OAuth 2, but I have recently learnt that OAuth2 was completely deprecated and its development was transferred to Spring Security 6.
When I run my project, I faced many issues at first, so I had to remove many deprecated libraries, functions, etc.

I have the following source code for my resource server:

@Configuration
@EnableWebSecurity
public class ResourceServerConfiguration {
    @Value("${spring.profiles.active}")
    private String activeProfile;

    @Value("${root.url.osb.basic.auth}")
    private String osbBasicAuth;

    @Value("${auth.server.url}")
    private String oauthServerUrl;

    @Value("${auth.server.clientId}")
    private String clientId;

    @Value("${auth.server.clientsecret}")
    private String clientSecret;


    public static final String BEARER_PREFIX = "Bearer ";
    public static final String HEADER_NAME = "Authorization";
    private final UserService userService;

    public ResourceServerConfiguration(UserService userService) {
        this.userService = userService;
    }

    @Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception {
        http.addFilterAfter(new OncePerRequestFilter() {
            @Override
            protected void doFilterInternal(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain filterChain)
                    throws ServletException, IOException {
                // We don't want to allow access to a resource with no token so clear
                // the security context in case it is actually an OAuth2Authentication
                var authHeader = request.getHeader(HEADER_NAME);
                if (StringUtils.isEmpty(authHeader) || !StringUtils.startsWith(authHeader, BEARER_PREFIX)) {
                    SecurityContextHolder.clearContext();
                }
                filterChain.doFilter(request, response);
            }
        }, AbstractPreAuthenticatedProcessingFilter.class);
//        http.csrf().disable();
        http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/", "/v1/open/**").permitAll());
        http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/pension/**").authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2.opaqueToken(opaqueTokenConfigurer ->
                opaqueTokenConfigurer.introspectionUri(oauthServerUrl)
                    .introspectionClientCredentials(clientId, clientSecret)));

        if (StringUtils.equalsIgnoreCase(activeProfile, "dev")) {
            http.authorizeHttpRequests(authorize ->
                authorize.requestMatchers("/webjars/**", "/resources/**", "/swagger-ui.html", "/swagger-resources/**", "/v2/api-docs", "index.html")
                .permitAll());
        }
        http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2.opaqueToken(opaqueTokenConfigurer ->
                opaqueTokenConfigurer.introspectionUri(oauthServerUrl)
                    .introspectionClientCredentials(clientId, clientSecret)));
        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        String[] authParts = osbBasicAuth.split(":");
        String username = authParts[0];
        String password = authParts[1];

        UserDetails userDetails = User.builder()
                .username(username)
                .password(new BCryptPasswordEncoder().encode(password)).roles()
                .build();
        return new InMemoryUserDetailsManager(userDetails);
    }
}

Once I get access token from the authorization server and try to submit request, it throws following error:

Caused by: org.springframework.web.client.ResourceAccessException: I/O error on POST request for "https://blabla/dev/agent-auth/oauth/": PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at org.springframework.web.client.RestTemplate.createResourceAccessException(RestTemplate.java:915)
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:895)
    at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:740)
    at org.springframework.security.oauth2.server.resource.introspection.SpringOpaqueTokenIntrospector.makeRequest(SpringOpaqueTokenIntrospector.java:151)
    ... 75 common frames omitted

I used to solve this problem in the past, when I used Spring boot 2 and Java 8, just by adding certificates to the keystore, but even in that moment the problem didn't disappear. Eventually, it turned out that problem was because I was adding certificates to a keystore in a wrong location. In this moment, I have double checked the location I add certificates and when I tried to submit request, the problem still exists.

JDK location
I add all certificates to cacerts in this location.

When I use .httpBasic(Customizer.withDefaults()) instead of oauth2ResourceServer and submit requests, the logs don't show anything.
On Spring Boot 2, there used to exist things such as RemoteTokenServices, where I believe that it helped to automatically solve problem when submitting requests to resource server with access tokens, but on Spring Boot 3, Spring Security 6 there is just so few information about the correct ways to implement the authentication.

Guides such as https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide
doesn't seem to give much details and they are ambiguous to understand.
Plus, all guides teaches how to implement authentication using JWT, but I want to implement it using simple Bearer token such as Opaque token auth in this tutorial: https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/opaque-token.html#oauth2resourceserver-opaque-introspectionuri-dsl

This is why, I ask for your help! Any help would be appreciated. Thanks!


Solution

  • After a long time of dealing with this problem I managed to solve it by replacing the URL I'm addressing to and export the trust certificate of that URL into truststore. If the original URL was https://blabla/dev/agent-auth/oauth/, later I replaced it with https://blabla/dev/agent-auth/oauth/check_token, which is a URL to validate a token in my project. It turned out that all mess was about the incorrect URL that I used to address.

    By the way, I managed to understand the correct implementation of the resource server configuration on Spring Security 6 which I posted on this link: OAuth2AuthenticatedPrincipal not loaded after introspect is executed I hope that you can find this link helpful for your own implementation.