Search code examples
spring-bootspring-securitycorsfetch-apipreflight

CORS Preflight Error: Working in Postman but Fails with Authenticated Spring Security


I'm trying to call an api to fetch user profile, but getting blocked by cors preflight error.

const username = 'username';
const password = 'password';
const credentials = `${username}:${password}`;
const base64Credentials = btoa(credentials);
const headers = {
    'Content-Type': 'application/json',
    'Authorization': `Basic ${base64Credentials}`,
    'Accept': '*/*'
}
const url = 'http://localhost:8080/account/' + username;
const method = "GET";

const callApi = () => {
    fetch(url, {
        method: method,
        headers: headers,
    }).then(res => {
        res.json().then(data => console.log(data));
    })
}

End point for getting user profile:

public ResponseEntity<ProfileDto> getUser(@PathVariable String username) {
            Optional<Account> accountOptional = accountRepository.findByUsername(username);
            if(accountOptional.isPresent()) {
                  Account account = accountOptional.get();
                  ProfileDto profileDto = new ProfileDto(
                          .....
                          .....
                          .....
                  );
                  return new ResponseEntity<>(profileDto, HttpStatus.FOUND);
            }
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
      }

Cors configuration:

@Configuration
public class WebConfig implements WebMvcConfigurer {
      @Override
      public void addCorsMappings(CorsRegistry registry) {
            registry
                    .addMapping("/**")
                    .allowedOrigins("http://localhost:5500")
                    .allowedMethods("GET", "POST")
                    .allowedHeaders("Content-Type", "Authorization", "Accept")
      }
}

Spring Security configuration:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
      @Bean
      public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
            httpSecurity
                    .csrf(AbstractHttpConfigurer::disable)
                    .exceptionHandling(exception -> exception
                            .authenticationEntryPoint(
                                    (req, res, ex) -> {
                                          res.sendError(
                                                  HttpServletResponse.SC_UNAUTHORIZED,
                                                  ex.getMessage()
                                          );
                                    }
                            )
                    )
                    .authorizeHttpRequests(auth -> auth
                            .requestMatchers( "/account").permitAll()
                            .anyRequest().authenticated()
                    )
                    .httpBasic(Customizer.withDefaults());
            return httpSecurity.build();
      }
}

I tried testing with postman. All api's are working when spring security is set to permit all but not working when set to authenticated.

httpSecurity.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())

authentication entry point hasn't detected the error.


Solution

  • The code works fine when the request doesn't need any authentication. But when the it need authentication security filter will validate it first. Since cors is not enabled in security filter, prefilght request fails.

    Here is what the documentation says.

    CORS must be processed before Spring Security because the pre-flight request will not contain any cookies (i.e. the JSESSIONID). If the request does not contain any cookies and Spring Security is first, the request will determine the user is not authenticated (since there are no cookies in the request) and reject it.

    To solve this, create a corsConfigurationSource bean and add it to to security filter.

    Here is modified code:

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig {
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
            httpSecurity.cors(Customizer.withDefaults());
            return httpSecurity.build();
        }
      
        @Bean
        public CorsConfigurationSource corsConfigurationSource() {
            CorsConfiguration configuration = new CorsConfiguration();
            configuration.addAllowedOrigin("http://127.0.0.1:5500");  // your domain
            configuration.setAllowedMethods(Arrays.asList("GET", "POST")); // yout methods
            configuration.setAllowedHeaders(Arrays.asList("Content-Type", "Authorization")); // your headers
            configuration.setAllowCredentials(true); // for cookies
      
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", configuration);
            return source;
        }
    } 
    

    By deafult httpSecurity.cors(Customizer.withDefaults()) searches for a bean named corsConfigurationSource. Refer docs for more.