Skip to content

Fix silent Keychain failures and form-encoding in auth - #26

Merged
adborbas merged 1 commit into
mainfrom
fix/auth-credentials
Jul 16, 2026
Merged

Fix silent Keychain failures and form-encoding in auth#26
adborbas merged 1 commit into
mainfrom
fix/auth-credentials

Conversation

@adborbas

Copy link
Copy Markdown
Owner

Changes

  • KeychainStore: introduce a KeychainStoring protocol seam; saveCredentials and clearAll now throw instead of swallowing errors with try?.
  • AuthManager.didAuthenticate: logs a persistence failure (Log.error) instead of pretending success.
  • AuthManager.clearKeychainIfReinstalled: sets the hasLaunchedBefore flag only after a confirmed Keychain wipe — a failed wipe is logged and retried on the next launch. performLogout's clear is wrapped in a logging do/catch.
  • TokenRefresher: logs a failed refreshed-credentials persist rather than discarding it.
  • TokenClient.formEncode: uses an explicit RFC 3986 unreserved CharacterSet (hoisted to a static let) so + & = , are escaped in the form body; .urlQueryAllowed left them raw (notably + → space).
  • Tests (Swift Testing): MockKeychainStore stub; AuthManager persist/wipe failure paths; formEncode escaping.

Why

A Keychain write failure was silently swallowed, so a user who completed login (or a background token refresh) could be dropped to onboarding with no explanation on the next cold launch — the "authenticated ⇒ persisted" invariant was violated invisibly. Separately, clearKeychainIfReinstalled set its first-run flag even when the wipe failed (so a failed wipe was never retried), and the form encoder left structural characters unescaped in the OAuth request body.

Notes

  • Reinstall upgrade trade-off (documented in-code): freshness is inferred only from the missing hasLaunchedBefore flag, which is equally absent for a legitimate upgrade from a pre-flag build — so such an upgrade triggers a one-time logout. Accepted knowingly for the safer security posture (never inherit foreign credentials). Confidence that a prior build lacked the flag is low (squashed history). No credential-inheritance heuristic was added.
  • didAuthenticate still marks the session authenticated on a persist failure (usable now, logged loudly) — the conservative option, not an abort. Flagged in case a hard precondition is preferred.
  • formEncode was made internal (from private) so it can be unit-tested via @testable import.
  • Verified: clean build + full HemeraTests suite green (9 new tests) on iPhone 16 Pro (iOS 18.4).

Three code-audit fixes in the auth layer, sharing one KeychainStore seam:

- KeychainStore: introduce `KeychainStoring` protocol; make
  `saveCredentials`/`clearAll` throw instead of swallowing errors with `try?`.
- AuthManager: `didAuthenticate` now logs a persist failure instead of
  pretending success; `clearKeychainIfReinstalled` sets the
  `hasLaunchedBefore` flag only after a confirmed wipe (a failed wipe is
  logged and retried next launch). The one-time-logout-on-upgrade trade-off
  is documented in-code (confidence low due to squashed history).
- TokenRefresher: log a failed refreshed-credentials persist.
- TokenClient.formEncode: use an explicit RFC 3986 unreserved CharacterSet so
  `+ & = ,` are escaped in a form body (`.urlQueryAllowed` left them raw).

Tests (Swift Testing): MockKeychainStore stub; AuthManager persist/wipe
failure paths; formEncode escaping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@adborbas
adborbas merged commit 793ff5b into main Jul 16, 2026
1 check passed
@adborbas
adborbas deleted the fix/auth-credentials branch July 16, 2026 20:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant