wimdeblauwe / htmx-spring-boot

Spring Boot and Thymeleaf helpers for working with htmx
Apache License 2.0
436 stars 41 forks source link

Support automatically adding CSRF token when using the custom processors #36

Open Paddy-Farmeye opened 1 year ago

Paddy-Farmeye commented 1 year ago

When submitting forms, Thymeleaf's th:action attribute adds required CSRF tokens automatically. It seems to me that this feature isn't supported when using, for example hx:post on a form.

For reference, it would eliminate the need to manually add <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /> to all of my newly-converted-to htmx forms.

Is this something that you envisage will be supported?

checketts commented 1 year ago

That seems like a worthwhile feature. With th:action Thymeleaf is generating the hidden input right? Would you expect the hx:post to do similarly? Or Perhaps use another HTMX feature to include the CSRF token?

Paddy-Farmeye commented 1 year ago

More information can be found at: https://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html#advanced-integration-features. But basically, Thymeleaf's Spring integration utilizes the RequestDataValueProcessor to transparently add hidden fields to "enable security features like e.g. protection agains CSRF (Cross-Site Request Forgery)."

th:action calls RequestDataValueProcessor.processAction(...) before rendering the form’s action attribute, and additionally it detects when this attribute is being applied on a form tag —which should be the only place, anyway—, and in such case calls RequestDataValueProcessor.getExtraHiddenFields(...) and adds the returned hidden fields just before the closing form tag.

wimdeblauwe commented 1 year ago

Would be nice to have this indeed!

Paddy-Farmeye commented 1 year ago

@wimdeblauwe do you see this feature being on this library's roadmap in the near future or...?

wimdeblauwe commented 1 year ago

If I would know how to do it, I would love to add it. I now asked https://github.com/thymeleaf/thymeleaf/discussions/934 if somebody might now the way how to do it.

checketts commented 1 year ago

Or the attribute process could add an extra hx-vals attribute that includes the CRSF token is another approach.

vrish88 commented 1 year ago

This could also be accomplished in htmx itself by adding an eventListener that adds the csrf token to the request. The spring security docs document how this might be done with a js library.

For HTMX it could be done with something like this (please forgive me, I haven't actually run this code):

<head>
  <meta name="_csrf" th:content="${_csrf.token}"/>
  <script>
    document.body.addEventListener('htmx:configRequest', function(evt) {
      evt.detail.headers['X-XSRF-TOKEN'] = document.querySelector('meta[name="_csrf"]').content
    });
  </script>
</head>

This would cover form submissions as well as requests triggered by the attributes hx-post, hx-get, hx-put, etc.

TomBeckett commented 1 year ago

I'm trying @vrish88's approach right now and it does look promising.

However, for some reason the CSRF token being sent is not the same token as the server expects in the CsrfFilter.class.

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
  // snip to line 61
  CsrfToken csrfToken = deferredCsrfToken.get();
  String actualToken = this.requestHandler.resolveCsrfTokenValue(request, csrfToken);

Also minor note, the token header name is slightly different for me (Spring Security 6).

It's also worth inserting the name into the header as this should adapt to Security config changes.

<head>
    <script src="https://unpkg.com/htmx.org@1.8.4"></script>
    <meta name="_csrf" th:content="${_csrf.token}"/>
    <meta name="_csrf_header" th:content="${_csrf.headerName}"/>
    <script>
    document.addEventListener("DOMContentLoaded", function() {
        var token = document.querySelector('meta[name="_csrf"]').content;
        var headerName = document.querySelector('meta[name="_csrf_header"]').content;
        document.body.addEventListener('htmx:configRequest', function(evt) {
          evt.detail.headers[headerName] = token;
        });
    });
    </script>
</head>

The token being sent automatically by Thymeleaf works, so something is clearly missing from this approach.

checketts commented 1 year ago

Is the CSRF token perhaps changing on different requests? Maybe each request needs to send along a new CSRF as a HX-Trigger that gets pulled in? Is the CSRF token static for an entire session or generated new with each request?

shimikano commented 1 year ago

Just for reference, in the spirit of the above javascript solutions to set the CSRF header for every htmx request, I've been using this:

<!--/* add the CSRF header to all htmx requests */-->
<script th:inline="javascript">
  /*<![CDATA[*/

  document.body.addEventListener('htmx:configRequest', (event) => {
    const csrfHeader = /*[[${_csrf.headerName}]]*/ 'X-Sample-CSRF-Header';
    const csrfToken = /*[[${_csrf.token}]]*/ 'sample-csrf-token';

    event.detail.headers[csrfHeader] = csrfToken;
  });

  /*]]>*/
</script>

This lets Thymeleaf inject the CSRF header name and token directly into the javascript template, as opposed to meta tags.

Works with Spring Boot 3.

dsyer commented 1 year ago

I'm pretty sure HTMX just sends all the form inputs (including the thymeleaf-generated ones), so as long as hx-post is inside a form it should just work? If you are converting forms to divs, then that's not very semantic HTML. I can see there might be a need for the occasional hx-post outside a form, but it should be the exception, right?

wimdeblauwe commented 1 year ago

A button that just triggers a delete of something might be in many cases not part of a form, but just an hx-delete on the button itself.

dsyer commented 1 year ago

That's a good example. If it isn't in a form, you can add the csrf token manually I guess, and tell HTMX about it with the hx-include. Personally I would do that, rather then getting into the weeds with JavaScript, but it's a matter of taste.

taypo commented 1 day ago

This is how I did it:

<button hx:vals="${ {_csrf.parameterName: _csrf.token } }" hx-post="...">Publish</button>