spring-addons
spring-addons copied to clipboard
Downstream services times out reading request body when csrf is set to cookie-accessible-from-js
Describe the bug
Downstream services times out reading request body of POST application/x-www-form-urlencoded request in BFF pattern when csrf: cookie-accessible-from-js
It works just fine if csrf is disabled, with exactly the same request going through the BFF.
I've just started using this project with version 7.6.11
, so it might be the case that I'm miss configuring something here or using it in a way that is not best practise. Or maybe I just missed a part in the documentation
Code sample
#Gateway/BFF route to resource server
spring:
cloud:
gateway:
routes:
- id: platform
uri: http://platform.127.0.0.1.nip.io:8081
predicates:
- Path=/**
filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
- TokenRelay=
#Gateway/BFF client config
com:
c4-soft:
springaddons:
oidc:
client:
clientUri: http://gateway.127.0.0.1.nip.io:8080/
security-matchers:
- /**
- /login/**
- /oauth2/**
- /logout
permit-all:
- /login/**
- /oauth2/**
csrf: cookie-accessible-from-js
#Resource server config for keycloak auth server
com:
c4-soft:
springaddons:
oidc:
ops:
- iss: http://auth.127.0.0.1.nip.io:8443/auth/realms/master
username-claim: $.preferred_username
authorities:
- path: $.realm_access.roles
- path: $.resource_access.*.roles
resourceserver:
cors:
- path: /**
allowed-origin-patterns: "*"
permit-all:
- "/actuator/health/readiness"
- "/actuator/health/liveness"
- "/v3/api-docs/**"
- "/swagger-ui/**"
Example request
curl "http://gateway.127.0.0.1.nip.io:8080/app/cake" ^
-H "Accept: */*" ^
-H "Accept-Language: en,en-GB;q=0.9,en-US;q=0.8" ^
-H "Cache-Control: no-cache" ^
-H "Connection: keep-alive" ^
-H "Content-Type: application/x-www-form-urlencoded" ^
-H "Cookie: XSRF-TOKEN=b6a23072-9a81-4b97-8c6c-e20da05c73f0; SESSION=654366d8-fa61-4ae9-8304-4c9452a7d660" ^
-H "HX-Current-URL: http://gateway.127.0.0.1.nip.io:8080/" ^
-H "HX-Request: true" ^
-H "Origin: http://gateway.127.0.0.1.nip.io:8080" ^
-H "Pragma: no-cache" ^
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" ^
-H "X-XSRF-TOKEN: b6a23072-9a81-4b97-8c6c-e20da05c73f0" ^
--data-raw "name=testName&type=testType" ^
--insecure
Expected behavior Downstream service should be able to read the incoming request body
Additional context
The only difference I can see in implementation and execution when the csrf is enabled and required, is that ServerCsrfTokenRequestHandler.resolveCsrfTokenValue()
is not invoked through the SpaCsrfTokenRequestHandler()
described in ReactiveConfigurationSupport
when csrf is set to COOKIE_ACCESSIBLE_FROM_JS
I think the core problem might be that this method of resolving the csrf first attempts to read it from the form data (even though the token is available in the header) in resolveCsrfTokenValue()
when exchange.getFormData()
is invoked.
As I understand, this body can only be read once, but it is cached. I thought this would mean that it would safely propagate to the downstream service, but this does not seem to be the case. As when this line is executed, the downstream service times out when attempting to read the request body.
I'm not sure I have a POST
request using application/x-www-form-urlencoded
. Do you have a reproducing sample somewhere?
It should just be a simple html form post. A quick standalone example in HTML rather than like the curl example above would be the following:
<!DOCTYPE html>
<html data-theme="dark" xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Please Log In</title>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<meta content="initial-scale=1.0" name="viewport"/>
<script crossorigin="anonymous" integrity="sha384-0gxUXCCR8yv9FM2b+U3FDbsKthCI66oH5IA9fHppQq9DDMHuMauqq1ZHBpJxQ0J0"
src="https://unpkg.com/[email protected]"></script>
<script>
window.addEventListener("DOMContentLoaded", (event) => {
document.body.addEventListener('htmx:configRequest', function (evt) {
evt.detail.headers ['X-XSRF-TOKEN'] = getCookie('XSRF-TOKEN');
});
});
function getCookie(name) {
let nameEQ = name + "=";
let ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
</script>
</head>
<body>
<div class="btn btn-primary" hx-post="/logout">logout</div>
<form hx-post="/app/foo" hx-swap="outerHTML">
<p>hello, <span></span></p>
<label for="name">Foo Name:</label><br>
<input id="name" name="name" type="text"><br>
<label for="type">Foo Type:</label><br>
<input id="type" name="type" type="text"><br>
<input type="submit" value="Submit">
</form>
</body>
</html>
For extra context on the reason for this structure:
From the BFF tutorial, it's putting the resource server behind the BFF gateway under the security matchers that would then also require CSRF, as opposed to the BFF's own resource server. From this example, the specific focus is on the /api/**
endpoints
spring:
cloud:
gateway:
routes:
- id: bff
uri: ${scheme}://${hostname}:${resource-server-port}
predicates:
- Path=/api/**
filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
- TokenRelay=
- SaveSession
- StripPrefix=1
com:
c4-soft:
springaddons:
oidc:
# Trusted OpenID Providers configuration (with authorities mapping)
ops:
- iss: ${issuer}
authorities:
- path: ${authorities-json-path}
aud: ${audience}
# SecurityFilterChain with oauth2Login() (sessions and CSRF protection enabled)
client:
client-uri: ${reverse-proxy-uri}${bff-prefix}
security-matchers:
- /api/**
- /login/**
- /oauth2/**
- /logout
permit-all:
- /api/**
- /login/**
- /oauth2/**
csrf: cookie-accessible-from-js
oauth2-redirections:
rp-initiated-logout: ACCEPTED
# SecurityFilterChain with oauth2ResourceServer() (sessions and CSRF protection disabled)
resourceserver:
permit-all:
- /login-options
- /error
- /actuator/health/readiness
- /actuator/health/liveness