user925406
user925406

Reputation: 65

Using OKTA - CORS Error After Upgrading to Spring Boot 3 - CORS header ‘Access-Control-Allow-Origin’ missing

I have an application with Vue.js in the front end and Spring Boot for the back end. We use OKTA for security. This has been working great with Java 11 and Spring Boot 2.1.8.

The Spring Boot REST services are http://localhost:7801 and the NGINX server for the UI is http://localhost:7800.

Recently, I am attempting to upgrade the back end to use Spring Boot 3.1.3 and Java 17. I get the following error when the UI accesses an end point:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:7801/oauth2/authorization/okta. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 403.

After updating Spring Boot, there were some required changes to Spring Security. Otherwise the back end code is identical. I do have http://localhost:7800 and http://localhost:7800/ configured in OKTA as Trusted Origins.

After researching this I am still not sure how to make this work. I appreciate any ideas.

Spring Boot 2.1.8

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OAuthSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .cors()
            .and().csrf().disable()
            .authorizeRequests()
            .antMatchers("/actuator/**").permitAll()
            .anyRequest().authenticated()
            .and().oauth2Client()
            .and().oauth2Login();
    }
}
@Bean
public WebMvcConfigurer corsConfigurer() {
    return new WebMvcConfigurer() {
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry
                    .addMapping("/api/myapp/**")
                    .allowedMethods("GET","POST","PUT","DELETE")
                    .allowedOrigins("http://localhost:7800");        
        }
    };
}

Spring Boot 3.1.3

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class OAuthSecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http              
                .cors(Customizer.withDefaults())
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests((authz) -> authz
                        .requestMatchers("/actuator/**").permitAll()                      
                        .anyRequest().authenticated())
                .oauth2Client(Customizer.withDefaults())
                .oauth2Login(Customizer.withDefaults());

         // done
        return http.build();
    }
@Bean
CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(List.of("http://localhost:7800"));
    configuration.setAllowedMethods(List.of("GET","POST","PUT","DELETE"));
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/api/myapp/**", configuration);
    return source;
} // end corsConfigurationSource()

Request Headers

OPTIONS /oauth2/authorization/okta HTTP/1.1
Host: localhost:7801
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/117.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Access-Control-Request-Method: GET
Access-Control-Request-Headers: authorization
Referer: http://localhost:7800/
Origin: http://localhost:7800
Connection: keep-alive
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site

Response Headers

HTTP/1.1 403 
Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Transfer-Encoding: chunked
Date: Thu, 07 Sep 2023 12:45:00 GMT
Keep-Alive: timeout=60
Connection: keep-alive

Console

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:7801/oauth2/authorization/okta. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 403.

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:7801/oauth2/authorization/okta. (Reason: CORS request did not succeed). Status code: (null).

Upvotes: 0

Views: 2471

Answers (2)

ch4mp
ch4mp

Reputation: 12554

Apparently, your conf allows cross origin requests to /api/myapp/** only and the failing request happens to /oauth2/authorization/okta

Also, instead of:

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http              
                .cors(Customizer.withDefaults())
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests((authz) -> authz
                        .requestMatchers("/actuator/**").permitAll()                      
                        .anyRequest().authenticated())

        ...

        return http.build();
    }

try:

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http, CorsConfigurationSource source) throws Exception {
        http              
                .cors(cors -> cors.configurationSource(source))
                .csrf(configurer -> {
                    final var delegate = new XorCsrfTokenRequestAttributeHandler();
                    delegate.setCsrfRequestAttributeName("_csrf");
                    // Adapted from https://docs.spring.io/spring-security/reference/5.8/migration/servlet/exploits.html#_i_am_using_angularjs_or_another_javascript_framework
                    configurer.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).csrfTokenRequestHandler(delegate::handle);
                    http.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class);
                }).authorizeHttpRequests((authz) -> authz
                        .requestMatchers("/login/**", "/oauth2/**", "/actuator/**").permitAll()                      
                        .anyRequest().authenticated())
                .oauth2Client(Customizer.withDefaults())
                .oauth2Login(Customizer.withDefaults());

        return http.build();
    }

    private static final class CsrfCookieFilter extends OncePerRequestFilter {

        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
                throws ServletException,
                IOException {
            CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
            // Render the token value to a cookie by causing the deferred token to be loaded
            csrfToken.getToken();

            filterChain.doFilter(request, response);
        }

    }

this will:

  • pick the corsConfigurationSource bean you already defined and explicitly inject it in your security filter-chain
  • enable protection against CSRF attacks
  • allow anonymous access to the URIs used during the login process (so, before the user is successfully authenticated)

With this modified CORS configuration source @Bean:

@Bean
CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(List.of("http://localhost:7800"));
    configuration.setAllowedMethods(List.of("*"));
    configuration.setAllowCredentials(true);
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/api/myapp/**", configuration);
    source.registerCorsConfiguration("/oauth2/**", configuration);
    source.registerCorsConfiguration("/logout", configuration);
    return source;
}

This will:

  • allow HEAD and OPTIONS requests too (and make your intentions clearer, probably).
  • allow CORS requests to some missing path
  • enable user credentials are support

P.S. n°1

Seems that you forgot to permitAll() access to /login/** and /oauth2/** which are used during the login process.

P.S. n°2

OAuth2 clients with oauth2Login must be protected against CSRF attacks: requests authorization is based on sessions which are the vector for CSRF attacks. Do not disable CSRF.

P.S. n°3

If you were using spring-cloud-gateway in front of both your Vue.js app and your Spring API, all requests would have the same origin: the gateway => no more CORS config needed.

The other benefit would be that you could configure the REST API as a stateless resource server (with CSRF protection disabled) while still using a "confidential" OAuth2 client and keep tokens on your servers only. I have a tutorial for configuring spring-cloud-gateway as such a BFF there. The frontend is written with Angular, but as all that is required regarding authorization is setting window.location.href, you should be able to port it to Vue.

This makes me strongly recommand you do introduce a BFF between your Vue.js app and your REST API:

  • use spring-cloud-gateway as stateful OAuth2 client with login and CSRF protection and TokenRelay filter
  • switch your REST API to resource server configuration (stateless and disabled CSRF)

The layer with sessions will be thinner and your overall architecture will scale much better.

Upvotes: 1

Matt Raible
Matt Raible

Reputation: 8614

You might try defining a FilterRegistrationBean like we did in our Build a Simple CRUD App with Spring Boot and Vue.js tutorial.

@Bean
public FilterRegistrationBean simpleCorsFilter() {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowCredentials(true);
    // *** URL below needs to match the Vue client URL and port ***
    config.setAllowedOrigins(Collections.singletonList("http://localhost:8080"));
    config.setAllowedMethods(Collections.singletonList("*"));
    config.setAllowedHeaders(Collections.singletonList("*"));
    source.registerCorsConfiguration("/**", config);
    FilterRegistrationBean bean = new FilterRegistrationBean<>(new CorsFilter(source));
    bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
    return bean;
}

We use something similar in JHipster.

FWIW, you don't need @EnableWebSecurity on your security configuration class if you're using Spring Boot. You only need @Configuration.

Upvotes: 0

Related Questions