Open Paddy-Farmeye opened 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?
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.
Would be nice to have this indeed!
@wimdeblauwe do you see this feature being on this library's roadmap in the near future or...?
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.
Or the attribute process could add an extra hx-vals
attribute that includes the CRSF token is another approach.
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.
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.
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?
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.
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?
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.
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.
This is how I did it:
<button hx:vals="${ {_csrf.parameterName: _csrf.token } }" hx-post="...">Publish</button>
I see a lot of solutions, but most of them are with Thymeleaf. What if you don't use Thymeleaf?
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 examplehx: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?