Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions docs/articles/antiforgery.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ see [Static assets and deployment](static-assets.md) for the URL and caching.

| Feature | HTMX versions | Without the script |
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------|
| Adds the antiforgery token to non-GET HTMX requests and refreshes it after boosted navigation | All | Non-GET requests without a token field fail server-side antiforgery validation, usually with HTTP 400 |
| Adds the antiforgery token to non-GET HTMX requests to the page's origin and refreshes it after boosted navigation | All | Non-GET requests without a token field fail server-side antiforgery validation, usually with HTTP 400 |
| Sends `HX-Request-Type`; see [Check full or partial requests](requests.md#check-full-or-partial-requests) | 1.9.x and 2.x; HTMX 4.x sends it itself | `IsHtmxPartialRequest()` and `IsHtmxFullRequest()` return `false`, and `[HtmxRequest]` with a `RequestType` does not match |
| Replays events whose name repeats in one response header; see [Timing and repeated events](responses.md#timing-and-repeated-events) | All | None of the events in that header fire |
| Adds the `ramstack-swap` extension with `innerMorph`, `outerMorph`, and a simplified `outerSync`, plus `textContent` for HTMX 1.9.x; see [Morph swaps](version-compatibility.md#morph-swaps) | 1.9.x and 2.x; HTMX 4.x supports these swap styles natively | These swap styles are not available |
Expand All @@ -21,7 +21,7 @@ see [Static assets and deployment](static-assets.md) for the URL and caching.
1. `<htmx-config />` asks ASP.NET Core for an antiforgery token.
2. The Tag Helper writes the request token, header name, and form-field name as data attributes on `<meta name="htmx-config">`.
3. The Toolkit script reads those attributes during page load.
4. Before each non-GET HTMX request, the script adds the token unless the form data already contains the configured antiforgery form field.
4. Before each non-GET HTMX request to the page's origin, the script adds the token unless the form data already contains the configured antiforgery form field.
5. The application validates the token through Razor Pages, an MVC filter, or its Minimal API validation setup.

The token is sent as the configured antiforgery header when a header name is available.
Expand Down Expand Up @@ -116,6 +116,24 @@ Keep `<htmx-config />` in full document layouts. After signing in, signing out,
obtain a token for the new identity. A full navigation with `HX-Redirect` is a straightforward way to do that;
see [Redirect after authentication](recipes.md#redirect-after-authentication). Keeping an old token does not refresh it.

## Requests to other origins

The script adds the token only to requests to the page's origin. Requests to another origin, including another
subdomain of the same site, do not receive it, so the token cannot leak to a third party.
If another origin of the application validates the same antiforgery tokens, send the token explicitly:

```razor
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery

<button hx-post="https://orders.example.com/orders"
hx-header-RequestVerificationToken="@Antiforgery.GetAndStoreTokens(Context).RequestToken">
Place order
</button>
```

Such a request also needs CORS with credentials on the other origin, and the antiforgery cookie must be
available to it.

## Static web assets and caching

Script loading, versioned URLs, and deployment are covered in [Static assets and deployment](static-assets.md).
Expand Down
6 changes: 5 additions & 1 deletion docs/articles/migrate-to-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ step; see [Version compatibility](version-compatibility.md) for that.
|---------------------------------------------------------------------|-------------------------------------------------------------------------------------|-------------------|
| [Registration](#register-the-toolkit-and-select-the-htmx-version) | Call `AddHtmxToolkit()` and select the HTMX version; HTMX 2.x is the default | **Silent** |
| [Global configuration](#move-global-configuration-to-code) | `<htmx-config>` attributes move to `AddHtmxToolkit()` | **Silent** |
| [Antiforgery](#antiforgery-metadata-is-on-by-default) | Metadata is included by default and no longer stored in `htmx.config` | **Silent** |
| [Antiforgery](#antiforgery-metadata-is-on-by-default) | Metadata on by default, moved out of `htmx.config`; token sent to own origin only | **Silent** |
| [Toolkit script](#load-the-toolkit-script-as-a-static-asset) | Inline script helpers and `MapHtmxAntiforgeryScript` replaced by a static web asset | Build |
| [URL Tag Helper](#rename-the-url-tag-helper-attributes) | `hx-*` URL attributes become `asp-*`; `hx-host` and `hx-protocol` are removed | **Silent** |
| [Requests](#update-request-handling) | `IsHtmxRequest()` is `false` for history restoration; `Boosted` filter renamed | Build, **silent** |
Expand Down Expand Up @@ -156,6 +156,10 @@ const { antiforgeryRequestToken, antiforgeryHeaderName } =
document.querySelector("meta[name='htmx-config']").dataset;
```

The Toolkit script now adds the token only to requests to the page's origin. In 1.x, it added the token
to every non-GET request, including requests to other origins. If another origin of the application validates
the same tokens, send the token explicitly; see [Requests to other origins](antiforgery.md#requests-to-other-origins).

See [Antiforgery and Toolkit script](antiforgery.md).

## Load the Toolkit script as a static asset
Expand Down
21 changes: 17 additions & 4 deletions src/Ramstack.HtmxToolkit/wwwroot/htmx-toolkit.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,21 @@ document._r_htmx ||= ((document, htmx) => {

let antiforgery = read_antiforgery(document);

const add_antiforgery = (method, headers, parameters) => {
if (!/^get$/i.test(method)) {
// XHR and fetch resolve a relative URL against the document base URL.
// A URL that cannot be parsed is treated as another origin.
const is_same_origin = url => {
try {
return new URL(url, document.baseURI).origin === location.origin;
}
catch {
return false;
}
};

// The token is sent only to the origin of the page; another origin, even a subdomain
// of the same site, must not receive it.
const add_antiforgery = (method, url, headers, parameters) => {
if (!/^get$/i.test(method) && is_same_origin(url)) {
const {
headerName,
formFieldName,
Expand Down Expand Up @@ -136,12 +149,12 @@ document._r_htmx ||= ((document, htmx) => {

listen("htmx:configRequest", e => {
let detail = e.detail;
add_antiforgery(detail.verb, detail.headers, detail.parameters);
add_antiforgery(detail.verb, detail.path, detail.headers, detail.parameters);
});

listen("htmx:config:request", e => {
let request = e.detail.ctx.request;
add_antiforgery(request.method, request.headers, request.body);
add_antiforgery(request.method, request.action, request.headers, request.body);
});

// HTMX reads hx-request from the element and its ancestors; the nearest value wins.
Expand Down
2 changes: 1 addition & 1 deletion src/Ramstack.HtmxToolkit/wwwroot/htmx-toolkit.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading