github ovumcy/ovumcy-web v1.1.0

8 hours ago

Ovumcy v1.1.0

Release date: 2026-05-23

Highlights

  • Security: TOTP step-up requirement on POST /auth/oidc/link-confirm closes a 2FA bypass where an attacker with the victim's password plus a malicious / sloppy upstream IdP could obtain a session for a TOTP-protected account without holding the second factor. The link-confirm form now requires a valid 6-digit code together with the password whenever the target account has TOTP enabled.
  • Bug: issue #64 — UTC-minus locales (e.g. America/Toronto, America/New_York) hit duplicated key not allowed on every second day-save, surfaced to the user as "failed to create day" / "auto-save failed". Root cause was double-canonicalization of the lookup window in UpsertDayEntry; consecutive day upserts now correctly update the canonical row.
  • Coverage: backend regression suite grows by 41 tests across security-sensitive surfaces (OIDC link-confirm flow, AEAD cookie codec for ovumcy_oidc_link_pending, DeleteDay handler, cross-user privacy boundaries, rate-limit responders, basic identity handlers, ShowResetPasswordPage). internal/api line coverage moves from 70.8% to 75.4%.

Security

  • TOTP step-up on /auth/oidc/link-confirm. When the target local-auth account has TOTP enabled, the link-confirmation submission must additionally carry a valid 6-digit totp_code form field. The handler runs TOTPService.CheckRateLimit and ValidateCode with the same per-(client_ip, user_id) failure counter and replay rejection (ErrTOTPReplayed) as POST /api/v1/sessions/2fa-challenge. The identity link and session cookie are only issued after both factors pass; on TOTP failure the pending-link cookie stays alive for retry within the 5-minute TTL, same as wrong-password. Previously the handler called AuthService.AuthenticateCredentials (password only) and went straight to setAuthCookie, allowing an attacker holding the victim's password plus a malicious or sloppy upstream IdP to obtain a session — and persist a linked OIDC identity — without ever holding the second factor.
  • AEAD codec coverage for ovumcy_oidc_link_pending. The cookie landed in v1.0.0 without dedicated codec regressions. v1.1.0 adds the canonical four-invariant lock (round-trip + cross-purpose AAD + tampered-byte + rotated-key) plus payload expiry and builder field validation, in line with the AEAD-sealed cookie coverage rule.
  • CSRF route-level locks. POST /auth/oidc/link-confirm and DELETE /api/v1/days/:date previously had handler-level coverage on a no-CSRF test app. v1.1.0 adds explicit regressions that run with the real CSRF middleware enabled and assert 403 when the csrf_token form field is missing, closing the SECURITY.md route-coverage requirement for both endpoints.
  • Cross-user privacy regressions. Owner B's GET /api/v1/symptoms must not surface Owner A's custom symptoms; Owner B's DELETE /api/v1/days/:date must not remove Owner A's row on the same calendar day. Both invariants were enforced at the service layer through user_id scoping but had no explicit regression — v1.1.0 adds one.
  • Sensitive-field leak guard on GET /api/v1/users/current. Adds an explicit deny-list assertion (password_hash, recovery_code_hash, totp_secret, $2a$, $2b$) so any accidental field-leak from a future refactor is caught immediately.

Fixed

  • days (issue #64): UpsertDayEntry was re-applying DayRange to a value already canonicalized to UTC-midnight by the caller. For UTC-minus locales DateAtLocation shifted the lookup window one day backward, so a second PUT /api/v1/days/{date} for the same calendar day missed the existing row and the follow-up Create collided with the uidx_user_date unique index. The handler now uses [dayStart, dayStart+24h) bounds directly, plus a defensive UTC-midnight normalization at the function entry so a future caller passing a non-canonical time.Time cannot reintroduce the same shift. Locked by integration tests on SQLite and Postgres (America/Toronto UTC-5 and Asia/Tokyo UTC+9 locales).

Changed

  • Link-confirm form (/auth/oidc/link-confirm) conditionally surfaces a TOTP input when TOTPRequired=true (computed in ShowOIDCLinkConfirmPage from the target user's TOTPEnabled). The new field reuses the auth.2fa.code_label / auth.2fa.code_placeholder i18n keys.
  • SECURITY.md gains a new ### OIDC Account Linking section in the Test Enforcement Matrix that links every claim about the link-confirm flow (password gate, TOTP gate, AAD-bound cookie, CSRF, local_auth_enabled=false refusal, MustChangePassword routing, audit-log emission) to the specific Go test that enforces it. The threat-model bullet on malicious-IdP account takeover is extended to mention the TOTP gate.
  • DeleteDay handler gains an idempotency lock: DELETE /api/v1/days/{date} on a day that has no row returns 204 (previously implicit behavior, now explicit regression).
  • Rate-limit responders test surface now covers the HTML fallback path on RespondAPIRateLimited in addition to the JSON envelope, and asserts the retry_after_seconds shape across auth-form / settings / global path routing.

Upgrade notes

  • OIDC operators with 2FA users. If you have local-auth accounts with TOTP enabled that may receive first-time OIDC links from a freshly-configured IdP, those users will now be required to enter a 6-digit code together with their password on /auth/oidc/link-confirm. The form surfaces the TOTP input automatically when the target account has TOTP enabled; no operator configuration change is needed.
  • UTC-minus deployments (anything west of UTC, e.g. Americas) — PUT /api/v1/days/{date} consecutive saves on the same calendar day now succeed instead of returning 500 on the second save. Users who saw "failed to create day" / "auto-save failed" toasts will stop seeing them after upgrade.
  • No new schema migrations in this release. v1.0.0 shipped through migration 022_register_pickup_tokens.sql; v1.1.0 only changes handler behavior, template form fields, and tests. Migration history is identical between v1.0.0 and v1.1.0.
  • Tagged images publish under ghcr.io/ovumcy/ovumcy-web:v1.1.0.
  • Existing deployments can upgrade in place by pulling the new image or by setting OVUMCY_IMAGE=ghcr.io/ovumcy/ovumcy-web:v1.1.0.

Full changelog

Don't miss a new ovumcy-web release

NewReleases is sending notifications on new releases.