I've lost my mind trying things here. This is only my latest attempt, I've basically been in "add" mode.
The documentation suggests this should be enough.
https://docs.spring.io/spring-security/reference/servlet/integrations/cors.html
// © Copyright 2024 Caleb Cushing
// SPDX-License-Identifier: AGPL-3.0-or-later
package com.xenoterracide.controller.authn;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository;
import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@EnableWebSecurity
@SpringBootApplication
public class ResourceServer {
ResourceServer() {}
public static void main(String[] args) {
SpringApplication.run(ResourceServer.class, args);
}
@Bean
@Order(0)
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/**")
.cors(cors -> cors.configurationSource(this.corsConfigurationSource()))
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.httpBasic(c -> c.disable())
.formLogin(f -> f.disable());
return http.build();
}
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.debug(true);
}
@Bean
HttpExchangeRepository httpExchangeRepository() {
return new InMemoryHttpExchangeRepository();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
var cors = new CorsConfiguration();
cors.addAllowedOrigin("http://localhost:3000");
cors.addAllowedMethod("*");
cors.addAllowedHeader("*");
var source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", cors);
return source;
}
@Bean
WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/external").allowedOrigins("http://localhost:3000");
}
};
}
@RestController
static class OidcTestController {
private final Logger log = LogManager.getLogger(this.getClass());
@CrossOrigin(originPatterns = "*")
@GetMapping("/api/external")
@NonNull
String index(@Nullable Authentication details) {
this.log.info("{}", details);
var name = details != null ? details.getName() : "world";
return "Hello, " + name;
}
}
}
INFO 743291 - Spring Security Debugger :
************************************************************
Request received for GET '/api/external':
org.apache.catalina.connector.RequestFacade@20882dd6
servletPath:/api/external
pathInfo:null
headers:
host: localhost:8080
user-agent: curl/8.6.0
accept: */*
Security filter chain: [
DisableEncodeUrlFilter
WebAsyncManagerIntegrationFilter
SecurityContextHolderFilter
HeaderWriterFilter
CorsFilter
CsrfFilter
LogoutFilter
BearerTokenAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
ExceptionTranslationFilter
AuthorizationFilter
]
************************************************************
DEBUG 743291 - o.spri.secu.web.FilterChainProxy : Securing GET /api/external
DEBUG 743291 - o.spri.secu.web.FilterChainProxy : Secured GET /api/external
DEBUG 743291 - o.spri.web.serv.DispatcherServlet : GET "/api/external", parameters={}
DEBUG 743291 - o.spri.web.serv.mvc.meth.anno.RequestMappingHandlerMapping : Mapped to com.xenoterracide.controller.authn.ResourceServer$OidcTestController#index(Authentication)
DEBUG 743291 - o.spri.secu.web.auth.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext
INFO 743291 - c.xeno.cont.auth.Reso.OidcTestController : null
DEBUG 743291 - ri.web.serv.mvc.meth.anno.RequestResponseBodyMethodProcessor : Using 'text/plain', given [*/*] and supported [text/plain, */*, application/json, application/*+json]
DEBUG 743291 - ri.web.serv.mvc.meth.anno.RequestResponseBodyMethodProcessor : Writing ["Hello, world"]
DEBUG 743291 - o.spri.web.serv.DispatcherServlet : Completed 200 OK
* Host localhost:8080 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:8080...
* Connected to localhost (::1) port 8080
> GET /api/external HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.6.0
> Accept: */*
>
< HTTP/1.1 200
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: 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
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 12
< Date: Fri, 05 Apr 2024 10:52:42 GMT
<
{ [12 bytes data]
* Connection #0 to host localhost left intact
Hello, world
Comment From: xenoterracide
So I finally tracked down my issue by stepping through the filter chain with the debugger... hrm... I opened another bug https://github.com/spring-projects/spring-framework/issues/32580 this seems like it's a logging issue as much as anything. I don't know how I was supposed to figure out the problem was that spring was being "smart" about header output. Leaving this open in case these docs could be improved, though I'm going to speculate I would have missed it unless it was loud. I have also asked for improvements to the docs in the gs guide https://github.com/spring-guides/gs-rest-service-cors/issues/39
Comment From: StevenPG
Is there an intermediate fix for something like actuator that you've found? I've been stuck on this for hours and had to create an incredibly janky implementation for a similar issue with spring-cloud-gateway.
Comment From: xenoterracide
Uh, I'm not sure if I understand the question/problem you're having. I tracked mine down to a header that needed to be sent in the request that is normally sent by a web browser, but it's not generally sent from other clients unless you tell it to.
Comment From: StevenPG
Sorry, I think I'm in the same boat. I have an empty project with spring-web-starter, spring-security-starter and a SecurityFilterChain. Following the documentation to the letter and can't make any progress into actually disabling cors successfully for RestController endpoints or actuator endpoints.
I also just think there must be something missing from the documentation, and can't figure out a way around it.
Comment From: xenoterracide
Sounds like the inverse of my problem which was getting the Cors headers to actually trigger
Comment From: sjohnr
Hi @xenoterracide, thanks for reaching out and sorry you had trouble with this. I also apologize for the delay in responding, but I'm going through our issue tracker now to find issues that were missed.
Leaving this open in case these docs could be improved
I feel the docs you linked to provide quite good examples and also link to Spring Framework's docs on the subject. Spring doesn't generally provide how-to guides on using web technologies and specifications themselves, so I don't think the information that would have helped is appropriate for our docs in any case.
I'm going to close this issue with the above explanation.