(I found similar issues but in my case even adding authorize("/error", authentication) or even authorize("/error", permitAll) doesn't help. I have no explicit session policy configuration.)
When using Spring Boot 2.7.3 with Spring Security Resource Server JWT, any exception that is eventually mapped to 401 or 403, results with an empty response body (I'm using the default ErrorAttributes from Spring Boot). I remember it working differently some time ago, but cannot nail the version that changed this.
What I would like the behavior to be: when the user is authenticated I would like to send 401 or 403 with request bodies. Is this possible?
Please see the attached example, run it with ./mvnw spring-boot:run. (I'm using a fake JWT decoder so that the sample doesn't require real tokens, it doesn't influence the behavior.) When you call http://localhost:8080/test/401 (or 403) you will get the response without the body, but using any other status does return the body.
Switch to basic auth (remember to disable the tokenDecoder @Bean) and it works fine.
Comment From: mbhave
Thanks for the sample. I'm not sure what the fake decoder is intended for but I don't think it's doing what it's supposed to do. If I hit http://localhost:8080/test/401, I don't get to the controller at all as Spring Security denies access.
Once access is denied to the controller, the difference in behavior for jwt and basic auth is due to the way Spring security's BearerTokenAuthenticationEntryPoint and BasicAuthenticationEntryPoint behave.
BasicAuthenticationEntryPoint calls response.sendError() which results in an error dispatch. If access to the error page is allowed, the response will contain a body. If not, it will just contain the original status code.
BearerTokenAuthenticationEntryPoint does not call response.sendError(). This means there is no error dispatch and Spring Boot's error handling infrastructure is not invoked.
However, it looks like the issue you're reporting is after Spring Security has been able to successfully authenticate a user. Can you modify the sample so that it actually does that with jwt authentication?
Comment From: wujek-srujek
You are right, sorry, I messed the sample up somehow. Please try the new sample attached. The folder contains a Postman collection with which you can:
- Get an access_token. When you invoke this, it will set the access_token collection variable.
- Invoke http://localhost:8080/test/402. It will automatically use the access_token collection variable set in the previous step.
To reproduce:
- Start with
./mvnw spring-boot:run. - Fetch a token using the Postman collection.
- Invoke the other request to
http://localhost:8080/test/402. You will see the body comes back. - Change the other request to invoke
http://localhost:8080/test/401or.../403. You will see that no body comes back. - Edit
src/main/kotlin/com/test/foo/Foo.ktand set/errortopermitAll, and redo the calls in steps 3 and 4. You should see that it makes no difference.
This means that even though the user is authenticated 401 or 403 don't result in sending an error body.
Comment From: mbhave
I wasn't able to run your sample (I'm not too familiar with how postman collections work). But I think what is missing is the RequestAttributeSecurityContextRepository configuration. Even though you haven't configured session management to be stateless explicitly, a JwtAuthenticationToken is annotated with @Transient which means it won't be saved by the HttpSessionSecurityContextRepository. Using a RequestAttributeSecurityContextRepository ensures that the original authentication is available on an error dispatch thereby allowing access to Spring Boot's error controller.
You can add RequestAttributeSecurityContextRepository to your security configuration as shown here.
Please let us know if that fixes the problem for you. We have an open issue for documenting this #30761.
Comment From: wujek-srujek
@mbhave You don't need to use Postman to test this, I just thought it would be helpful, but you can use whatever you like. Here is an example with curl:
- Start the server using
./mvnw spring-boot:run - Invoke:
curl --request POST 'https://dev-arg0-cet.us.auth0.com/oauth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Cookie: did=s%3Av0%3A746b8e10-30d0-11ed-a1d5-8f38b5a150f7.ozVaQs3tQb6tEeTOaueXkG2bGcHZYXxNZtGEeDxyzvA; did_compat=s%3Av0%3A746b8e10-30d0-11ed-a1d5-8f38b5a150f7.ozVaQs3tQb6tEeTOaueXkG2bGcHZYXxNZtGEeDxyzvA' \
--data-urlencode 'client_id=KaGx9BUOBQ692FG1MLgRDtLUIcI2FMUs' \
--data-urlencode 'client_secret=HCpWio9nwoLopX0AnVFGksdTmULrH1COqEGCWMQA08qz1IedqkiASRBcjaRhUdln' \
--data-urlencode 'audience=https://test.spring/api' \
--data-urlencode 'grant_type=client_credentials'
to get the token.
3. You will get a JSON back, copy the value of the field access_token and invoke this:
curl --request GET 'http://localhost:8080/test/402' --header 'Authorization: Bearer <the access token value copied in previous step>'
You will see that the request to http://localhost:8080/test/402 returns the body in the response. Requests to http://localhost:8080/test/401 or http://localhost:8080/test/403 never return a body.
I will try your suggestion when I'm at my computer, thanks.
Comment From: wujek-srujek
Yes, that was it, the repository fixed the problem. Thank you for looking into this.
I remember reading about the repository in the docs before but I didn't understand how this would affect me and why I would need it, so I never configured it. Maybe some examples about why one would need it would be helpful?
Comment From: mbhave
Thanks for letting us know that it worked for you @wujek-srujek. We have an open issue to add documentation for this #30761.