skip to Main Content

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).

2

Answers


  1. 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.

    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search