From 9f27775da99ee5869bb2bc728ef63ac30c7c7b82 Mon Sep 17 00:00:00 2001 From: null Date: Sun, 31 May 2026 16:08:24 -0500 Subject: [PATCH] oidc error correction --- HISTORY.md | 20 ++++++++------------ routes/authOidc.js | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 4e07230..a359551 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,17 +1,5 @@ # Bill Tracker — Changelog -## v0.36.0 - -### 🔧 Changed - -- **Bump** — `0.35.0` → `0.36.0` - -### 🔒 Security - -- **CSRF token moved out of readable cookie** — The CSRF cookie previously defaulted to `httpOnly: false` so the SPA could read it from `document.cookie`. Any XSS vulnerability could steal the token from there and bypass CSRF protection entirely. The cookie is now `httpOnly: true` by default, removing it from the XSS-accessible cookie surface. The SPA instead fetches the token once from `GET /api/auth/csrf-token` on startup and stores it in a module-level memory cache; all mutations continue to send it in the `x-csrf-token` header unchanged. The server-side double-submit validation (`header == cookie`) is identical. `CSRF_HTTP_ONLY=false` remains available in `.env` for compatibility, but is no longer the default. - ---- - ## v0.35.0 ### 🔧 Changed @@ -24,6 +12,11 @@ - **HKDF key derivation with automatic migration** — `encryptionService.js` previously derived the AES-256-GCM key from raw input via `SHA-256(ikm)`, which lacks domain separation and offers no protection if a low-entropy passphrase is supplied. Replaced with **HKDF-SHA-256** (RFC 5869) using info label `bill-tracker-token-encryption-v1`. New ciphertext carries a `v2:` prefix; `decryptSecret` uses it to choose the correct derivation path, so legacy and new ciphertext coexist transparently. DB migration `v0.78` re-encrypts all existing secrets (`data_sources.encrypted_secret` and `notify_smtp_password`) to the v2 format on first startup — no manual action required. +- **CSRF token moved out of readable cookie** — The CSRF cookie previously defaulted to `httpOnly: false` so the SPA could read it from `document.cookie`. Any XSS vulnerability could steal the token from there and bypass CSRF protection entirely. The cookie is now `httpOnly: true` by default, removing it from the XSS-accessible cookie surface. The SPA instead fetches the token once from `GET /api/auth/csrf-token` on startup and stores it in a module-level memory cache; all mutations continue to send it in the `x-csrf-token` header unchanged. The server-side double-submit validation (`header == cookie`) is identical. `CSRF_HTTP_ONLY=false` remains available in `.env` for compatibility, but is no longer the default. + +### Release Image + +![Doing my part](/img/doingmypart.jpg) --- ## v0.34.3 @@ -70,6 +63,9 @@ - **Monetary aggregation rounding hardened** — Floating-point rounding was already applied per-payment in `statusService`, `billsService`, and `payments.js`, but the aggregation layer was unprotected: `reduce()` sums and subtraction results in `trackerService`, `routes/summary.js`, and `routes/monthly-starting-amounts.js` were returned without rounding, allowing IEEE 754 artifacts (e.g. `12.10 - 0.20 = 12.100000000000001`) to leak into API responses. All computed monetary aggregates (`total_paid`, `total_expected`, `paid_toward_due`, `remaining`, `total_remaining`, `overdue`, `combined_amount`, `*_remaining`, `expense_total`, `result`, etc.) are now passed through `Math.round(x * 100) / 100` before being returned. `roundMoney` is also now exported from `statusService` so other modules can share the same implementation instead of re-implementing it. +### Release Image + +![Doing my part](/img/doingmypart.jpg) --- ## v0.34.2 diff --git a/routes/authOidc.js b/routes/authOidc.js index 030234b..e524c00 100644 --- a/routes/authOidc.js +++ b/routes/authOidc.js @@ -40,7 +40,12 @@ router.get('/login', async (req, res) => { const authUrl = await buildAuthorizationUrl(config, state); res.redirect(authUrl); } catch (err) { - console.error('[oidc] Login initiation error:', err.message); + const msg = err.message || String(err); + console.error('[oidc] Login initiation error:', msg || '(no message)'); + if (err.errors) { + err.errors.forEach((e, i) => console.error(` [${i}]`, e.message || String(e))); + } + if (err.cause) console.error(' cause:', err.cause.message || String(err.cause)); res.status(502).json({ error: 'Failed to reach the identity provider. Please try again.' }); } }); @@ -98,7 +103,12 @@ router.get('/callback', async (req, res) => { res.redirect(savedState.redirect_to || '/'); } catch (err) { // Log message only — never log tokens, codes, or ID token contents - console.error('[oidc] Callback error:', err.message); + const msg = err.message || String(err); + console.error('[oidc] Callback error:', msg || '(no message)'); + if (err.errors) { + err.errors.forEach((e, i) => console.error(` [${i}]`, e.message || String(e))); + } + if (err.cause) console.error(' cause:', err.cause.message || String(err.cause)); const errCode = err.status === 403 ? 'access_denied' : 'authentication_failed'; res.redirect(`/?oidc_error=${errCode}`); }