Some checks failed
Run Check Script / check (pull_request) Failing after 38s
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>
31 lines
806 B
TOML
31 lines
806 B
TOML
[package]
|
|
name = "harmony_zitadel_auth"
|
|
edition = "2024"
|
|
version.workspace = true
|
|
readme.workspace = true
|
|
license.workspace = true
|
|
|
|
[features]
|
|
default = []
|
|
axum = ["dep:axum", "dep:axum-extra"]
|
|
|
|
[dependencies]
|
|
anyhow.workspace = true
|
|
base64.workspace = true
|
|
chrono = { workspace = true, features = ["serde"] }
|
|
rand.workspace = true
|
|
reqwest.workspace = true
|
|
serde.workspace = true
|
|
serde_json.workspace = true
|
|
sha2 = "0.10"
|
|
url.workspace = true
|
|
tokio = { workspace = true, features = ["time"] }
|
|
arc-swap = "1"
|
|
time = "0.3"
|
|
tracing = { workspace = true }
|
|
|
|
jsonwebtoken = "9"
|
|
openidconnect = { version = "4", default-features = false, features = ["reqwest", "rustls-tls"] }
|
|
axum = { version = "0.8", optional = true }
|
|
axum-extra = { version = "0.10", features = ["cookie", "cookie-private"], optional = true }
|