Using Spring Boot 2.7 (Spring Security 5.7.1) and trying to configure an API as a resource server and OAuth2 client I find a behavior I don't get to understand:
#EnableWebSecurity
public class SecurityConfig {
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.mvcMatchers("/swagger-ui/**", "/api-docs/**").permitAll()
.anyRequest().permitAll())
// register OAuth2 resource server
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
// register OAuth2 client
.oauth2Client(withDefaults());
return http.build();
}
}
Checking the logs, all this filters apply
o.s.s.web.DefaultSecurityFilterChain : Will secure any request with
org.springframework.security.web.session.DisableEncodeUrlFilter#320ca97c,
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter#3c592c0c,
org.springframework.security.web.context.SecurityContextPersistenceFilter#2b33e616,
org.springframework.security.web.header.HeaderWriterFilter#2e9bff08,
org.springframework.security.web.csrf.CsrfFilter#7926d092,
org.springframework.security.web.authentication.logout.LogoutFilter#37227aa7,
org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter#6f18445b,
org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter#42af2977,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter#79e3f444, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter#1252d480,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter#3979c6e8,
org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter#19faa9dc,
org.springframework.security.web.session.SessionManagementFilter#7d3b4646,
org.springframework.security.web.access.ExceptionTranslationFilter#6cb2d5ea,
So far, this configuration works as expected in the other APIs I'm protecting. However, in this particular one, AND not having protected any endpoint I see that:
I can access any GET endpoint but any POST endpoint returns a 403 FORBIDDEN. However, I can access them if I add a token to the request EVEN if its an expired token
This alone I can't understand as .anyRequest().permitAll() should unprotect anything, if I'm not wrong.
If I comment out the line configuring the oauth2 ResourceServer
`// .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)`
this filter dissapears
org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter
And can't use POST endpoints anymore, even with the expired token
Logically, I want the API to be oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) so
How it is I can't access POST endpoints when using .anyRequest().permitAll())?
DISCLAIMER: I know it makes no sense to declare the API as resource server if all endpoints must be public. Endpoints will be accessed by Discord callbacks and I have to figure out if I can protect them with OAuth
EDIT:
server.servlet.context-path = /api
Controller
#RestController
#RequestMapping("/slack")
public class SlackBotController {
#PostMapping("/test")
public String test(#RequestBody String a) {
return a;
}
#GetMapping("/test")
public String testGet() {
return "OK";
}
}
Request
GET/POST http://localhost:8081/api/slack/test
Request headers
User-Agent: PostmanRuntime/7.29.0
Accept: */*
Postman-Token: f20ba7a6-26e5-47c4-a827-0596afec27b8
Host: localhost:8081
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 3
Cookie: JSESSIONID=D1C2B2668DC130C63DDE03F30574ED5F; JSESSIONID=823D79956CFBF14F3C77C96E29F4131C
Response headers
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 03 Jun 2022 12:03:00 GMT
Keep-Alive: timeout=60
Connection: keep-alive
The reason why the POST endpoint or the POST returns a 403 FORBIDDEN because the CSRF protection is enabled by default in spring.
That means that every modifying request (POST, PUT, DELETE, PATCH) requires a CSRF token. Otherwise the request gets denied to prevent CSRF attacks.
To disable the CSRF protection:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable();
}
The problem here is the CSRF protection. When you use http.oauth2ResourceServer() Spring Security configures CSRF to ignore requests that contains the header Authorization: Bearer whatever, note that it has to contain the Bearer prefix.
In the request sample that you shared, you are not including the Authorization header.
Take a look at the code in Spring Security.
We have a Spring Boot application that runs in Openshift where we configure the Cache-Control header like this:
#Configuration
#EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().cacheControl().disable().addHeaderWriter((httpServletRequest, httpServletResponse) -> {
httpServletResponse.setHeader(HttpHeaders.CACHE_CONTROL, "public, max-age=86400");
});
}
}
In the HTTP responses there are two Cache-Control headers:
$ curl --header https://<our-url> --head
HTTP/1.1 200 Connection established
HTTP/1.1 200
...
Cache-Control: public, max-age=86400
...
Cache-control: private
We expect the first header, and we have no idea where the second header comes from. (Note the lowercase c in the name of the second header.)
Any ideas where the second header comes from and how we can get rid of it?
I found the answer: The Cache-control header is added by HAProxy. HAProxy uses this header and a cookie to create sticky sessions (i.e. to make sure that requests from the same client are handled by the same pod).
See this question for details.
In short, you can disable this behaviour by
oc annotate route <myroute> haproxy.router.openshift.io/disable_cookies='true'
I am using Spring Boot with Spring Security. Request TRACE http://localhost:8080/invalid/path gets mapped /error in my spring boot application. Can I know where exactly this request gets mapped to /error. I want to handle the request to send custom response. I enabled the debugging for web security by the following line.
public void configure(WebSecurity web) throws Exception {
web.debug(true);
}
Output of curl -D - -X TRACE http://localhost:8081/invalid/path
HTTP/1.1 405
Allow: HEAD, DELETE, POST, GET, OPTIONS, PUT
Content-Type: message/http
Content-Length: 83
Date: Thu, 14 May 2020 06:24:25 GMT
TRACE /error HTTP/1.1
host: localhost:8104
user-agent: curl/7.64.1
accept: */*
The application log shows the following
************************************************************
Request received for TRACE '/error':
org.apache.catalina.core.ApplicationHttpRequest#7dba82cf
servletPath:/error
pathInfo:null
headers:
host: localhost:8104
user-agent: curl/7.64.1
accept: */*
Security filter chain: [] empty (bypassed by security='none')
************************************************************
My Custom Firewall ignores the RequestRejectedException
#Override
public FirewalledRequest getFirewalledRequest(final HttpServletRequest request) {
try {
return super.getFirewalledRequest(request);
} catch (RequestRejectedException ex) {
return new FirewalledRequest(request) {
#Override
public void reset() {}
};
}
}
Any help on this ?
Using Spring Boot 2.2.6
I faced the same issue, here is the solution:
Adding this config into the WebSecurity configuration.
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests().antMatchers("/error/**").permitAll();
}
I have the class below which uses antMatchers to remove authentication from a public endpoint.
However the public endpoint is also being blocked and I keep getting a HTTP/1.1 401.
Can anyone please help me to spot what's wrong below?
#Configuration
#EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
#Autowired
private UsersService usersService;
#Autowired
private UsersRepository usersRepo;
#Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{
return new BCryptPasswordEncoder();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable();
http.authorizeRequests().antMatchers(HttpMethod.POST, "/public").permitAll()
.anyRequest().authenticated()
.and().addFilter(getAuthenticationFilter());
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(usersService).passwordEncoder(bCryptPasswordEncoder());
}
private AuthenticationFilter getAuthenticationFilter() throws Exception {
final AuthenticationFilter filter = new AuthenticationFilter(authenticationManager(),
usersService);
return filter;
}
}
---------------update 1-------------------
I tried with http POST, using curl and I get back the below.
It seems like the request is caught somewhere but not in the controller I am trying to hit:
$ curl -X POST http://localhost:8083/public -H 'Content-Type:
application/json' -H 'cache-control: no-cache' -d '{
"email":"test2#test.com", "password":"12345678" }' -v
* Trying ::1:8083...
* Connected to localhost (::1) port 8083 (#0)
> POST /user HTTP/1.1
> Host: localhost:8083
> User-Agent: curl/7.69.1
> Accept: */*
> Content-Type: application/json
> cache-control: no-cache
> Content-Length: 51
>
* upload completely sent off: 51 out of 51 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 401
< Set-Cookie: JSESSIONID=72AB25425322A17AE7014832D25284FD; Path=/;
HttpOnly
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< WWW-Authenticate: Basic realm="Realm"
< Content-Length: 0
< Date: Tue, 31 Mar 2020 11:36:10 GMT
<
* Connection #0 to host localhost left intact
You might want to override the WebSecurity method to completely ignore your /public path from Spring Security processing.
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/public/**");
}
It is difficult to know without seeing all of your code, but I suspect that this has nothing to do with the authorizeRequests() portion of your configuration. Instead, I suspect it is the AuthenticationFilter which is attempting to authenticate the request because you have included credentials in the request. The default is to try to authenticate anytime the AuthenticationConverter returns credentials. Then AuthenticationFailureHandler will respond with HTTP 401 if invalid credentials are provided.
To resolve this, you can remove the credentials from your request. Alternatively, you can limit which requests AuthenticationFilter are invoked on by setting the requestMatcher. Something like this would limit to processing POST to /authenticate:
private AuthenticationFilter getAuthenticationFilter() throws Exception {
final AuthenticationFilter filter = new AuthenticationFilter(authenticationManager(),
usersService);
filter.setRequestMatcher(new AntPathRequestMatcher("/authenticate", "POST"));
return filter;
}
While debugging a CORS issue I am experiencing I've found the following behaviour. Chrome makes the following OPTIONS preflight request (rewritten in CURL by Chrome itself):
curl -v 'https://www.example.com/api/v1/users' -X OPTIONS -H 'Access-Control-Request-Method: POST' -H 'Origin: http://example.com' -H 'Accept-Encoding: gzip,deflate,sdch' -H 'Accept-Language: es-ES,es;q=0.8,en;q=0.6' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36' -H 'Accept: */*' -H 'Referer: http://example.com/users/new' -H 'Connection: keep-alive' -H 'Access-Control-Request-Headers: accept, x-api-key, content-type'
The response from the server to this request if the following:
< HTTP/1.1 403 Forbidden
< Date: Thu, 21 Jul 2016 14:16:56 GMT
* Server Apache/2.4.7 (Ubuntu) is not blacklisted
< Server: Apache/2.4.7 (Ubuntu)
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< Strict-Transport-Security: max-age=31536000 ; includeSubDomains
< X-Frame-Options: SAMEORIGIN
< Allow: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH
< Content-Length: 20
< Keep-Alive: timeout=5, max=100
< Connection: Keep-Alive
being the body of the response 'Invalid CORS request'. If I repeat the request removing the header 'Access-Control-Request-Method' (and only that header) the OPTIONS requests succeeds with the following reponse:
< HTTP/1.1 200 OK
< Date: Thu, 21 Jul 2016 14:21:27 GMT
* Server Apache/2.4.7 (Ubuntu) is not blacklisted
< Server: Apache/2.4.7 (Ubuntu)
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< Strict-Transport-Security: max-age=31536000 ; includeSubDomains
< X-Frame-Options: SAMEORIGIN
< Access-Control-Allow-Headers: origin, content-type, accept, x-requested-with, x-api-key
< Access-Control-Max-Age: 60
< Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
< Access-Control-Allow-Origin: *
< Allow: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH
< Content-Length: 0
< Keep-Alive: timeout=5, max=100
< Connection: Keep-Alive
However, the offending header is a CORS spec standard header so it should not prevent the request from succeeding, right? Why is this header causing such behaviour?
And how can I tweak the access control headers sent by my server to make the request work when made with Chrome?
By the way, I am using Chrome 36.0, and the server is using Spring Boot, with the CORS headers being managed by Spring.
When the request is made by Firefox (v47.0) the behaviour is different but with an analogue result. Firefox does not even send the preflight request, it directly sends the POST request, which receives as response a 403 Forbidden. However, if I copy the request with the 'Copy as cURL' option, and repeat it from a terminal window, It succeeds and sends the correct CORS headers in the response.
Any idea?
Update: Firefox does send the preflight OPTIONS request (as shown by the Live HTTP headers plugin), but Firebug masks it, so the behaviour in both browsers it exactly the same. In both browsers is the 'Access-control-request-method' header the difference that makes the request fail.
After a lot of struggling, I finally found the problem. I configured a request mapping in Spring to handle OPTIONS traffic, like this:
#RequestMapping(value= "/api/**", method=RequestMethod.OPTIONS)
public void corsHeaders(HttpServletResponse response) {
response.addHeader("Access-Control-Allow-Origin", "*");
response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.addHeader("Access-Control-Allow-Headers", "origin, content-type, accept, x-requested-with");
response.addHeader("Access-Control-Max-Age", "3600");
}
I did not know that by default Spring uses a default CORS processor, and it seems it was interfering with my request mapping. Deleting my request mapping and adding the #CrossOrigin annotation to the appropriate request mappings solved the problem.
i also faced the same issue and find solution for enabling global cors issue in spring boot
#Configuration
#EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedMethods("GET", "POST", "PUT", "DELETE").allowedOrigins("*")
.allowedHeaders("*");
}
}
after this , we need to enable CORS in spring security level also, so for this
add cors() in your SecurityConfiguration class which extent WebSecurityConfigurerAdapter
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.cors()
.and()
.csrf().disable()
.authorizeRequests()..
}
I had the same issue. I've resolve it by adding 'OPTIONS' to allowed CORS methods in my Spring MVC configuration.
#Configuration
#EnableWebMvc
#ComponentScan
public class RestApiServletConfig extends WebMvcConfigurerAdapter {
#Override
public void addCorsMappings(CorsRegistry registry) {
super.addCorsMappings(registry);
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000", "http://localhost:8080")
.allowedMethods("GET", "PUT", "POST", "DELETE", "OPTIONS");
}
}
For me I have added #crossorigin annotation in each of controller api call.
#CrossOrigin
#PostMapping(path = "/getListOfIndividuals", produces = { "application/json" }, consumes = { "application/json" })
public ResponseEntity<String> test(#RequestBody String viewIndividualModel)
throws Exception {
String individualDetails = globalService.getIndividualDetails(viewIndividualModel);
finalString = discSpecAssmentService.getViewFormForDisciplineEvaluation( viewIndividualModel);
return new ResponseEntity<String>(finalString, HttpStatus.OK);
}
Edit: Enable CORS in security configuration and make sure options requests bypass security
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.cors()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// More security configuration here
}
I added this as an answer because I couldn't format it well for the top voted answer.
I found this post helpful as well: How to handle HTTP OPTIONS with Spring MVC?
DispatchServlet must be configured to pass along options request, or else it never reaches the mapped request:
...
<servlet>
<servlet-name>yourServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>dispatchOptionsRequest</param-name>
<param-value>true</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
...
I came across this really while testing the CORS on our endpoints using test-cors.org website and it exhibits the exact same behavior that is described above.
The approach that I did was to use the Global CORS filter instead of using the #CrossOrigin annotation.
#Configuration
class CorsConfig : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/**")
.allowCredentials(true)
.allowedHeaders("*")
.allowedMethods("*")
.allowedOrigins("*")
.maxAge(3600)
}
}
Note that you should not use #EnableWebMvc unless you want to take control Spring Boot Auto-configuration as noted here...which will probably cause some "issues" as noted here and here
This next custom configuration is also needed (solution partially lifted from here) or else you will get that particular CORS pre-flight issue:
#Configuration
class CustomWebSecurityConfigurerAdapter : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http.cors().and().csrf().disable()
}
}