P_M
P_M

Reputation: 2942

Spring security Access-Control-Allow-Origin: * (CORS) issue on invalid JWT token

I have JWT security configured with Spring Security in Spring Boot application. I have an issue with

Access-Control-Allow-Origin: *

header, also known as CORS. I configured application so header is present in every server response but once JWT token invalid, server responses with 403 error code without Access-Control-Allow-Origin: * header. This leads browser to write error message to console:

Failed to load http://... No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://...' is therefore not allowed access. The response had HTTP status code 403.

It seems wrong and I would like to get Access-Control-Allow-Origin: * header in response, even though JWT token invalid and server responses with 403 error code.

Now what I tried and my code.

Dependencies:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.5.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
...

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

As I understood this issue could be caused by filter order in filter chain, and I tried to put my JWT JwtAuthenticationFilter after CorsFilter or CsrfFilter, create CorsConfigurationSource bean. This described at https://docs.spring.io/spring-security/site/docs/current/reference/html5/#cors and discussed at How to configure CORS in a Spring Boot + Spring Security application? and https://github.com/spring-projects/spring-boot/issues/5834, but nothing seems helps

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Value("${com.faircloud.common.security.header}")
    private String header;
    @Value("${com.faircloud.common.security.prefix}")
    private String prefix;
    @Value("${com.faircloud.common.security.validateLink}")
    private String validateLink;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().authorizeRequests()
                .antMatchers("/v2/api-docs", "/configuration/ui", "/swagger-resources/**", "/configuration/**",
                        "/swagger-ui.html", "/webjars/**")
                .permitAll()
                .and().authorizeRequests().anyRequest().authenticated().and()
                .addFilterAfter(new JwtAuthenticationFilter(header, prefix, validateLink),
                        CsrfFilter.class)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        final CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(ImmutableList.of("*"));
        configuration.setAllowedMethods(ImmutableList.of("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH"));
        // setAllowCredentials(true) is important, otherwise:
        // The value of the 'Access-Control-Allow-Origin' header in the response must
        // not be the wildcard '*' when the request's credentials mode is 'include'.
        configuration.setAllowCredentials(true);
        // setAllowedHeaders is important! Without it, OPTIONS preflight request
        // will fail with 403 Invalid CORS request
        configuration.setAllowedHeaders(ImmutableList.of("Authorization", "Cache-Control", "Content-Type"));
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

Here the JwtAuthenticationFilter class. Please note, to validate token it calls other microservice over http. Also my application does not have login endpoint, because login implemented on other microservice application.

public class JwtAuthenticationFilter extends BasicAuthenticationFilter {

    private String header;
    private String prefix;
    private String validateLink;

    public JwtAuthenticationFilter(String header, String prefix, String validateLink) {
        super(new AuthenticationManager() {
            public Authentication authenticate(Authentication authentication) throws AuthenticationException{
                return null;
            }
        });
        this.header = header;
        this.prefix = prefix;
        this.validateLink = validateLink;
    } 

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
            HttpServletResponse response, 
            FilterChain chain)
            throws ServletException, IOException {

        // 1. get the authentication header. Tokens are supposed to be passed in the
        // authentication header
        String headerValue = request.getHeader(header);

        // 2. validate the header and check the prefix
        if (headerValue == null || !headerValue.startsWith(prefix)) {
            chain.doFilter(request, response); // If not valid, go to the next filter.
            return;
        }
        // 3. Get the token     
        String token = headerValue.replace(prefix, ""); 

        try {

            GatewayResponse gatewayResponse = validate(token);

            String userId = gatewayResponse.getUserId();

            /*
            Roles could come from gateway or loaded from current
            microservice database by user id. They are
            hardcoded here to illustrate how to populate
            SecurityContextHolder
            */
            List<String> authorities = new LinkedList<String>();
            authorities.add("USER");
            authorities.add("ADMIN");

            UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userId, null,
                    authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()));
            SecurityContextHolder.getContext().setAuthentication(auth);
            addTokenToResponse(gatewayResponse.getAuthHeader(), response);
        } catch (Exception e) {
            // In case of failure. Make sure it's clear; so guarantee user won't be
            // authenticated
            SecurityContextHolder.clearContext();
        }

        // go to the next filter in the filter chain
        chain.doFilter(request, response);
    }

    private void addTokenToResponse(String authHeaderValue, HttpServletResponse response) {
        response.addHeader(header, prefix+authHeaderValue);
    }

    private GatewayResponse validate(String token) {
        /HTTP call here, returns null if invalid token
        ...
    }
}

Upvotes: 4

Views: 4749

Answers (2)

darshakat
darshakat

Reputation: 689

Try to use the CrosFilter. Find the below sample.

@Bean
public CorsFilter corsFilter() {
    final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    final CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowCredentials(true);
    configuration.addAllowedOrigin("*");
    configuration.addAllowedHeader("*");
    configuration.addAllowedMethod("OPTIONS");
    configuration.addAllowedMethod("HEAD");
    configuration.addAllowedMethod("GET");
    configuration.addAllowedMethod("PUT");
    configuration.addAllowedMethod("POST");
    configuration.addAllowedMethod("DELETE");
    configuration.addAllowedMethod("PATCH");
    source.registerCorsConfiguration("/**", configuration);
    return new CorsFilter(source);
}

Upvotes: 1

vsapiha
vsapiha

Reputation: 176

having similar issue, couldn't make it working with CorsConfigurationSource. Only filter based CORS support helped:

@Bean
public FilterRegistrationBean filterRegistrationBean() {
    final CorsConfiguration config = new CorsConfiguration();

    config.setAllowCredentials(true);
    config.addAllowedOrigin("http://localhost:4200");
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");

    final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);

    FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
    bean.setOrder(Ordered.HIGHEST_PRECEDENCE);

    return bean;
}

Upvotes: 5

Related Questions