All checks were successful
Run Check Script / check (pull_request) Successful in 2m18s
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.
220 lines
8.3 KiB
Markdown
220 lines
8.3 KiB
Markdown
# 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](./operator-dashboard-sso.md).
|
|
|
|
## 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:
|
|
|
|
```js
|
|
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:
|
|
|
|
```http
|
|
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:
|
|
|
|
```http
|
|
X-Content-Type-Options: nosniff
|
|
Referrer-Policy: same-origin
|
|
Permissions-Policy: geolocation=(), microphone=(), camera=()
|
|
```
|
|
|
|
When the service is HTTPS-only, also set HSTS:
|
|
|
|
```http
|
|
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:
|
|
|
|
```text
|
|
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.
|
|
|
|
## 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=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.
|