harden: widget edge cases — expired session, account switch, cache
All checks were successful
Security Checks / dependency-audit (push) Successful in 14s
Security Checks / secret-scanning (push) Successful in 3s
Security Checks / dockerfile-lint (push) Successful in 4s

1. Session expires: widget gets 401 → clears stale cookie from
   App Group → stops retrying with bad auth → shows cached data
   until user opens app and re-authenticates

2. Account switch: login() now calls clearWidgetAuth() BEFORE
   syncCookieToWidget() — clears previous user's cached calories
   before writing new user's cookie. No brief display of wrong data.

3. Logout: already correct — clearWidgetAuth removes cookie +
   cached data, widget shows 0/2000

4. Minimum data: only session cookie + 2 cached numbers + timestamp
   in App Group. No passwords, no user IDs, no PII.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-04 11:31:44 -05:00
parent e21a26db18
commit 5d51ac6833
2 changed files with 10 additions and 2 deletions

View File

@@ -77,6 +77,7 @@ final class AuthManager {
currentUser = response.user currentUser = response.user
isLoggedIn = true isLoggedIn = true
UserDefaults.standard.set(true, forKey: loggedInKey) UserDefaults.standard.set(true, forKey: loggedInKey)
clearWidgetAuth() // Clear previous user's cached data
syncCookieToWidget() syncCookieToWidget()
WidgetCenter.shared.reloadAllTimelines() WidgetCenter.shared.reloadAllTimelines()
} }

View File

@@ -116,8 +116,15 @@ struct CalorieProvider: TimelineProvider {
do { do {
let (data, response) = try await URLSession.shared.data(for: request) let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse, guard let http = response as? HTTPURLResponse else { return nil }
(200...299).contains(http.statusCode) else { return nil }
// Session expired clear stale cookie so we don't retry
if http.statusCode == 401 {
sharedDefaults.removeObject(forKey: "widget_sessionCookie")
return nil
}
guard (200...299).contains(http.statusCode) else { return nil }
return try JSONSerialization.jsonObject(with: data) as? [String: Any] return try JSONSerialization.jsonObject(with: data) as? [String: Any]
} catch { } catch {
return nil return nil