5

I have spring security + CORS enable into a project that is made with spring webFlux. My problem here is that we accept for example requests from: http://localhost:4200. How I can make that CORS will accept reqs from http://*.localhost:4200 like http://a.localhost:4200, http://b.localhost:4200 ?

My CORS config looks like:

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public CorsWebFilter corsFilter() {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowCredentials(true);

    config.setAllowedOrigins(corsConfigData.getAllowedOrigins());
    config.setAllowedHeaders(corsConfigData.getAllowedHeaders());
    config.setAllowedMethods(corsConfigData.getAllowedMethods());

    source.registerCorsConfiguration("/**", config);
    return new CorsWebFilter(source);
}

Do you have any ideas ???

brebDev
  • 774
  • 10
  • 27
  • 1
    Look at similar post https://stackoverflow.com/questions/27147737/cors-filter-allow-all-sub-domains answer by miko Spring web (>version 5.3) has CorsConfiguration.setAllowedOriginPatterns(..) to support wildcards. – user2025527 Jan 19 '21 at 11:55

3 Answers3

12

I think I found a solution that works. That simply means creating a custom CorsConfiguration, overriding the checkOrigin method and create a custom matcher that will interpret http://*.localhost:4200 correctly. The code looks like this:

public class RegexCorsConfiguration extends CorsConfiguration {

private List<String> allowedOriginsRegexes = new ArrayList<>();

/**
 * Check the origin of the request against the configured allowed origins.
 * @param requestOrigin the origin to check
 * @return the origin to use for the response, possibly {@code null} which
 * means the request origin is not allowed
 */
public String checkOrigin(String requestOrigin) {
    if (!StringUtils.hasText(requestOrigin)) {
        return null;
    }

    if (this.allowedOriginsRegexes.isEmpty()) {
        return null;
    }

    if (this.allowedOriginsRegexes.contains(ALL)) {
        if (getAllowCredentials() != Boolean.TRUE) {
            return ALL;
        } else {
            return requestOrigin;
        }
    }

    for (String allowedOriginRegex : this.allowedOriginsRegexes) {
        if (createMatcher(requestOrigin, allowedOriginRegex).matches()) {
            return requestOrigin;
        }
    }

    return null;
}

public void setAllowedOriginRegex(List<String> allowedOriginsRegexes) {
    this.allowedOriginsRegexes = allowedOriginsRegexes;
}

private Matcher createMatcher(String origin, String allowedOrigin) {
    String regex = this.parseAllowedWildcardOriginToRegex(allowedOrigin);
    Pattern pattern = Pattern.compile(regex);
    return pattern.matcher(origin);
}

private String parseAllowedWildcardOriginToRegex(String allowedOrigin) {
    String regex = allowedOrigin.replace(".", "\\.");
    return regex.replace("*", ".*");
}}

and of course, inject corsConfig from configuration classes like this:

    @Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public CorsWebFilter corsFilter() {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    RegexCorsConfiguration regexCorsConfiguration = new RegexCorsConfiguration();
    regexCorsConfiguration.setAllowCredentials(true);

    regexCorsConfiguration.setAllowedOriginRegex(corsConfigData.getAllowedOrigins());
    regexCorsConfiguration.setAllowedHeaders(corsConfigData.getAllowedHeaders());
    regexCorsConfiguration.setAllowedMethods(corsConfigData.getAllowedMethods());

    source.registerCorsConfiguration("/**", regexCorsConfiguration);
    return new CorsWebFilter(source);
}
brebDev
  • 774
  • 10
  • 27
  • 2
    Nice solution for springboot. I'd only suggest to compile patterns just once inside setAllowedOriginRegex() and keep list of Pattern objects instead of Strings, as this compiling is a bit performance hungry and it's not good idea doing it over and over again everytime you need to process a request. Otherwise thanks for nice idea :o) – Kuba Apr 01 '20 at 08:23
2

I think, as indicated in responses to this question, the CORS specification doesn't allow for wildcarding a subdomain. Specifically see https://www.w3.org/TR/cors/#access-control-allow-origin-response-header

You could follow their advice on that answer and move the processing into some middleware layer like NGINX or Apache which could set the CORS header dynamically based on the domain in the request, or specify all the the subdomains you'd want in the spring boot config if that doesn't total into an unmanageable amount.

Although, in the first part of your question you state that you accept requests from http://localhost:4200., this shouldn't be a problem if you don't need subdomains then you can just explicitly whitelist that one domain, or did I misunderstand?

David
  • 7,652
  • 21
  • 60
  • 98
  • Basically, all our other services accept sub-domains, so this is why I'm trying to find a solution for this new service. Yep so I need something like http://something.localhost:4200 to be accepted. There are some solution like CrossOriginFilter from org.eclipse.jetty.servlets, but we are not any more into a Servlet stack. We started this new service with spring WebFlux so we are in a reactive stack and that solution doesn't work anymore.. – brebDev Nov 21 '18 at 13:46
  • I see, I think the main point is that you can't use a header value of `http://*.localhost:4200`. You can achieve your end goal, but you'd either have to explicitly tell spring all of the permitted subdomains or implement some more custom solution to intercept the request, determine the origin header value, decide whether it's permitted for CORS and if so append the relevant header to the response. You might be able to write your own interceptor for this rather than using the spring cors features. I'm not aware of any built in feature of spring to allow that out of the box. – David Nov 21 '18 at 14:02
  • I think I managed to find out a solution. Please take a look above. – brebDev Nov 22 '18 at 14:55
2
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public CorsWebFilter corsFilter() {
   UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowCredentials(true);

    config.setAllowedOrigins(corsConfigData.getAllowedOrigins());
    config.setAllowedHeaders(corsConfigData.getAllowedHeaders());

    //Set allowed patterns here
    config.setAllowedPatterns(List.of("http://*.localhost:4200"));

    source.registerCorsConfiguration("/**", config);
    return new CorsWebFilter(source);
}

Since 5.3 you can setAllowedPatterns alternative to setAllowedOrigins that supports more flexible origins patterns with "*" anywhere in the host name in addition to port lists. Examples:

  • https://*.domain1.com -- domains ending with domain1.com
  • https://*.domain1.com:[8080,8081] -- domains ending with domain1.com on port 8080 or port 8081
  • https://*.domain1.com:[*] -- domains ending with domain1.com on any port, including the default port
btorkoy
  • 21
  • 2