Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Downstream services times out reading request body when csrf is set to cookie-accessible-from-js #203

Open
FusionHS opened this issue Apr 8, 2024 · 2 comments
Assignees
Labels
bug Something isn't working

Comments

@FusionHS
Copy link

FusionHS commented Apr 8, 2024

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.

@FusionHS FusionHS added the bug Something isn't working label Apr 8, 2024
@ch4mpy
Copy link
Owner

ch4mpy commented Apr 8, 2024

I'm not sure I have a POST request using application/x-www-form-urlencoded. Do you have a reproducing sample somewhere?

@FusionHS
Copy link
Author

FusionHS commented Apr 8, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants