Files
harmony/docs/guides/web-auth-security.md
Reda Tarzalt 96e7d43b2f
Some checks failed
Run Check Script / check (pull_request) Failing after 38s
add auth to frontend through lib (#284)
Adds OIDC login support to the harmony-fleet-operator web dashboard using Zitadel SSO.

pkce was the recommended option for this since we don't need to hold on to any secret. We compute a value on server before sending the data to Zitadel who validates authenticity by recomputing the hash and comparing the two values.

pkce Auth flow

 1. User visits a protected dashboard route, like /devices.
 2. If no valid harmony_fleet_session cookie exists, the app redirects to /login.
 3. /login creates:
     - random state
     - random pkce_code_verifier
     - derived code_challenge = base64url(sha256(pkce_code_verifier))
 4. The app stores state and pkce_code_verifier in a temporary HTTP-only login-attempt cookie.
 5. The browser is redirected to Zitadel’s authorize endpoint with:
     - client_id
     - redirect_uri
     - scope
     - state
     - code_challenge
     - code_challenge_method=S256
 6. After SSO login, Zitadel redirects back to /auth/callback?code=...&state=....
 7. The callback handler:
     - parses the raw query into a strict success/failure enum
     - reads the temporary login-attempt cookie
     - validates returned state
     - exchanges code + pkce_code_verifier for tokens
     - validates the returned ID token using OIDC discovery/JWKS
     - creates a local harmony_fleet_session cookie
     - redirects to /
 8. Protected routes validate the local dashboard session cookie on each request.
 9. /logout clears the dashboard session cookie and redirects to /login.

---

 Auth middleware responses depending on request type:

 - normal browser request: redirect to /login
 - SSE request: 401 authentication required
 - HTMX request: 401 with HX-Redirect: /login (HTMX redirect is more idiomatic than through Axum for this)

Reviewed-on: #284
Reviewed-by: johnride <jg@nationtech.io>
Co-authored-by: Reda Tarzalt <tarzaltreda@gmail.com>
Co-committed-by: Reda Tarzalt <tarzaltreda@gmail.com>
2026-05-19 20:37:08 +00:00

8.1 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.

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:

  1. OIDC Authorization Code + PKCE for login.
  2. OIDC nonce validation on login callback.
  3. Explicit authorization checks using roles, groups, claims, or permissions.
  4. CSRF protection on all mutating routes.
  5. Secure cookie settings: HttpOnly, Secure in production, constrained SameSite, and appropriate path/domain scoping.
  6. Strict security headers, especially Content Security Policy.
  7. No permissive credentialed CORS for operator dashboards.
  8. 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:

  • state
  • pkce_code_verifier
  • nonce
  • creation timestamp or cookie expiration

Send state, PKCE challenge, and nonce to the authorization endpoint.

On callback:

  1. Require a valid login-attempt cookie.
  2. Validate returned state against the stored state.
  3. Exchange the authorization code using the stored PKCE verifier.
  4. 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
  5. Create the application session only after all checks pass.
  6. Delete the login-attempt cookie.

state and nonce are not interchangeable:

  • state binds the callback redirect to the browser login attempt.
  • nonce binds 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:

  • HttpOnly
  • Secure outside local development
  • SameSite=Lax or SameSite=Strict
  • Path=/ unless a narrower path is possible
  • No broad Domain attribute 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:viewer for read-only dashboard access
  • fleet:operator for alert acknowledgement and operational actions
  • fleet:admin for 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:

  1. Require a custom header on all mutating requests.
  2. Validate Origin or Referer against the configured application origin.
  3. Keep cookies SameSite=Lax or stricter.
  4. Do not enable permissive credentialed CORS.

Mutating methods are:

  • POST
  • PUT
  • PATCH
  • DELETE

Recommended behavior:

  • Reject mutating requests without x-csrf-token.
  • Reject mutating requests whose Origin is present and does not match the configured base URL origin.
  • If Origin is absent, require Referer to match the configured base URL origin.
  • Reject when neither Origin nor Referer is 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, Secure in production, and SameSite=Lax or stricter.
  • No permissive credentialed CORS is enabled.
  • CSP is configured without unsafe-inline where practical.
  • Security headers are configured.
  • Auth errors shown to users are generic.
  • Detailed auth failures are logged server-side.

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=Lax or Strict session cookie.
  • CSRF middleware requiring x-csrf-token and same-origin Origin/Referer.
  • Static /static/app.js that adds the HTMX CSRF header.
  • Strict CSP that allows scripts only from self.
  • No CORS unless explicitly reviewed.