Next.js with Spring Boot OAUTH2 JWT: calls API from localhost:3000 with OPTIONS with no Bearer causing 401

We have implemented the next.js Auth0 quickstart and modified to call our AWS EBS hosted RESTAPI . Nexts.js is issuing an OPTIONS call before the GET without a Bearer token. Spring Boot is authenticating with .oauth2ResourceServer but the BearerTokenAuthenticationFilter is rejecting the OPTIONS before the cors filter can allow it.

Questions:

  • Do I need to write a Filter to go before the BearerTokenAuthenticationFilter and what would I do in it ?
    Is there a way to have OPTIONS NOT go through regular JWT authentication before the CORS filter is applied ?

My Security.Config:

/**
 * Configures our application with Spring Security to restrict access to our API endpoints.
 */
@Configuration
@EnableWebSecurity
@EnableGlobalAuthentication 

public class SecurityConfig {
    static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);

    @SuppressWarnings("deprecation")
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        /*
        This is where we configure the security required for our endpoints and setup our app to serve as
        an OAuth2 Resource Server, using JWT validation.
        */
        String username = "";
        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

        if (principal instanceof UserDetails) {
            username = ((UserDetails)principal).getUsername();
        } else {
            username = principal.toString();
        }
        logger.info("\n\nUser Name: {}\n\n", username);
        
        return http
                .cors(Customizer.withDefaults())
                .authorizeRequests((authorize) -> authorize
                    .requestMatchers("/users").authenticated()
                    .requestMatchers("/public").permitAll()
                    .requestMatchers("/get-dashboard-embed").authenticated()
                    .requestMatchers("/private").authenticated()
                    .requestMatchers("/private-scoped").hasAuthority("SCOPE_read:messages")
                )
                .cors(cors -> cors.configurationSource(request -> {
                    CorsConfiguration configuration = new CorsConfiguration();
                    configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
                    configuration.setAllowedMethods(Arrays.asList("GET, POST, OPTIONS"));
                    configuration.setAllowedHeaders(Arrays.asList("*"));
                    return configuration;
                }))
                .oauth2ResourceServer(oauth2 -> oauth2
                    .jwt(withDefaults())
                )
                .build();
    }
}

The log output for the DefaultSecurtyFilterChain is:

2024-04-16T20:34:04.327-04:00  INFO 23388 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter@60a4595e, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@4a234e1f, org.springframework.security.web.context.SecurityContextHolderFilter@4b0df349, org.springframework.security.web.header.HeaderWriterFilter@6441da2c, org.springframework.security.web.csrf.CsrfFilter@2f98859a, org.springframework.security.web.authentication.logout.LogoutFilter@369f85c6, org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter@6919a8a, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@48463900, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@1a1d7832, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@406cc81b, org.springframework.security.web.access.ExceptionTranslationFilter@5c883b3f, org.springframework.security.web.access.intercept.AuthorizationFilter@766ab25b]