Step-by-step for wiring the operator dashboard's browser SSO: the Zitadel app settings the code requires (Web app, PKCE/no-secret, redirect + post-logout URIs), each config value mapped to its source (ZitadelAuthConfig + cookie key), how to provide them (staging via FleetDeployConfig/Secrets with hosts derived from base_domain; local via HARMONY_CONFIG_* env), the derived endpoints, and the common-failure gotchas (iss/aud/redirect mismatch, no client secret, localhost dev mode, ≥64-byte cookie key). Grounded in harmony_zitadel_auth's login/jwks code. Registered in SUMMARY and cross-linked from web-auth-security.
8.3 KiB
Web Authentication and CSRF Security Guidelines
These guidelines define the baseline for Harmony web frontends and future operator dashboards that use browser-based authentication, cookie sessions, Axum, HTMX, or OIDC providers such as Zitadel.
Setting up the fleet operator dashboard's SSO concretely (Zitadel app + the exact config values)? See Operator Dashboard SSO (Zitadel) — setup.
Goals
- Prevent unauthenticated access.
- Prevent authenticated users from performing actions they are not authorized to perform.
- Prevent CSRF on state-changing endpoints.
- Reduce XSS impact with CSP and safe rendering practices.
- Keep authentication code understandable and reusable across projects.
Required Baseline
Every browser-facing authenticated application must implement the following controls before production use:
- OIDC Authorization Code + PKCE for login.
- OIDC nonce validation on login callback.
- Explicit authorization checks using roles, groups, claims, or permissions.
- CSRF protection on all mutating routes.
- Secure cookie settings:
HttpOnly,Securein production, constrainedSameSite, and appropriate path/domain scoping. - Strict security headers, especially Content Security Policy.
- No permissive credentialed CORS for operator dashboards.
- Generic client-facing errors with detailed errors logged server-side only.
OIDC Login Requirements
Use Authorization Code flow with PKCE. On login start, generate and persist a short-lived login attempt containing:
statepkce_code_verifiernonce- creation timestamp or cookie expiration
Send state, PKCE challenge, and nonce to the authorization endpoint.
On callback:
- Require a valid login-attempt cookie.
- Validate returned
stateagainst the stored state. - Exchange the authorization code using the stored PKCE verifier.
- Validate the returned ID token as an OIDC ID token, including:
- signature
- issuer
- audience/client ID
- expiration/not-before
- nonce
- authorized party (
azp) when applicable
- Create the application session only after all checks pass.
- Delete the login-attempt cookie.
state and nonce are not interchangeable:
statebinds the callback redirect to the browser login attempt.noncebinds the returned ID token to the browser login attempt.- PKCE binds the code exchange to the client that started the flow.
Session Requirements
For small internal dashboards, a verified short-lived ID token in an HttpOnly cookie may be acceptable. For higher-risk systems, prefer server-side sessions:
- Store a random session ID in the browser cookie.
- Store tokens and session metadata server-side.
- Support revocation, rotation, idle timeout, and absolute timeout.
Session cookies must use:
HttpOnlySecureoutside local developmentSameSite=LaxorSameSite=StrictPath=/unless a narrower path is possible- No broad
Domainattribute unless explicitly required
Production services should fail closed if HTTPS/secure-cookie configuration is inconsistent.
Authorization Requirements
Authentication is not authorization. A valid identity provider token only proves who the user is.
Every protected application must define required permissions for each state-changing or sensitive route. Examples:
fleet:viewerfor read-only dashboard accessfleet:operatorfor alert acknowledgement and operational actionsfleet:adminfor settings, user management, or destructive actions
Authorization must be enforced server-side. UI hiding is not sufficient.
CSRF Protection Standard
For Axum + HTMX dashboards, the recommended baseline is:
- Require a custom header on all mutating requests.
- Validate
OriginorRefereragainst the configured application origin. - Keep cookies
SameSite=Laxor stricter. - Do not enable permissive credentialed CORS.
Mutating methods are:
POSTPUTPATCHDELETE
Recommended behavior:
- Reject mutating requests without
x-csrf-token. - Reject mutating requests whose
Originis present and does not match the configured base URL origin. - If
Originis absent, requireRefererto match the configured base URL origin. - Reject when neither
OriginnorRefereris available, unless the route is explicitly exempted and documented.
The CSRF header value may be static for HTMX dashboards, for example x-csrf-token: 1. The protection comes from the fact that cross-origin HTML forms cannot set custom headers, and cross-origin JavaScript cannot send custom headers with credentials unless CORS allows it.
Do not rely on header presence alone if adding Origin/Referer validation is practical.
HTMX Integration
Add the CSRF header globally from a static JavaScript file:
document.body.addEventListener('htmx:configRequest', (event) => {
event.detail.headers['x-csrf-token'] = '1';
});
Serve this as a static asset, for example /static/app.js. Avoid inline scripts so that the application can use a strict CSP without unsafe-inline.
Content Security Policy
Every browser-facing dashboard should set a restrictive CSP. A good starting point is:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'
Meaning:
- Only load scripts, styles, and API/SSE/HTMX connections from the same origin.
- Prevent clickjacking with
frame-ancestors 'none'. - Prevent plugin/object execution with
object-src 'none'. - Prevent injected
<base>tags from rewriting relative URLs. - Prevent forms from submitting to external origins.
If inline scripts or styles are unavoidable, prefer per-response nonces over unsafe-inline.
Other Security Headers
Set these headers on all HTML responses, or globally when safe:
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Permissions-Policy: geolocation=(), microphone=(), camera=()
When the service is HTTPS-only, also set HSTS:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Only enable HSTS when the domain and subdomains are intended to be HTTPS-only.
CORS Policy
Operator dashboards should normally not enable CORS.
Never combine all of the following unless there is a reviewed, explicit integration need:
- credentialed requests
- arbitrary or reflected origins
- custom request headers such as
x-csrf-token
A permissive credentialed CORS policy can bypass custom-header CSRF protection.
Error Handling
Client-facing auth errors should be generic, for example:
Authentication failed. Please start login again.
Detailed causes, provider responses, token validation failures, and stack traces should be logged server-side only.
Avoid returning raw OIDC provider error bodies or JWT validation details to the browser.
Implementation Checklist
Before shipping a Harmony web frontend:
- Login uses Authorization Code + PKCE.
- Login attempt stores
state, PKCE verifier,nonce, and expires quickly. - Callback validates
state. - Callback validates ID token nonce.
- JWT validation checks issuer and exact intended audience/client.
- Authorization roles/permissions are enforced server-side.
- Mutating routes are protected by CSRF middleware.
- CSRF middleware requires custom header and same-origin
Origin/Referer. - Session cookies are
HttpOnly,Securein production, andSameSite=Laxor stricter. - No permissive credentialed CORS is enabled.
- CSP is configured without
unsafe-inlinewhere practical. - Security headers are configured.
- Auth errors shown to users are generic.
- Detailed auth failures are logged server-side.
Recommended Default for Harmony Dashboards
For current and future Axum + HTMX dashboards, use this default design:
- Zitadel/OIDC Authorization Code + PKCE + nonce.
- Short-lived encrypted login-attempt cookie.
- Server-side authorization middleware based on roles/claims.
HttpOnly,Secure,SameSite=LaxorStrictsession cookie.- CSRF middleware requiring
x-csrf-tokenand same-originOrigin/Referer. - Static
/static/app.jsthat adds the HTMX CSRF header. - Strict CSP that allows scripts only from
self. - No CORS unless explicitly reviewed.