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