Audit and fix AkFit security stability issues#28
Conversation
8-domain verified audit (architecture, performance, tests/CI, App Store, offline/state, Supabase scale, HealthKit, UX) with adversarial verification of all P0/P1 findings, live-DB/advisor/log evidence. Top finding: password reset is a complete dead end (permanent lockout). Includes scores, top-10, exact recommended tests, and a roadmap with an explicit not-worth-fixing list. No fixes implemented in this phase. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
App code uses UserDefaults in 7 files (GuestDataStore, NotificationService, AuthManager, four stores) — a required-reason API Apple enforces via ITMS-91053 at upload validation. SDK manifests (supabase-swift, sentry-cocoa) cover SDK code only, so a missing app-level manifest could block any future upload, including an urgent hotfix, at archive time. Declares only what the app actually uses: UserDefaults with reason CA92.1 (app's own data), tracking=false, no tracking domains. A repo sweep confirmed no other required-reason APIs (file timestamps, boot time, disk space, keyboard list) in app code. Verified present in the built app bundle root with correct content. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign-out previously reset NO stores, and the manual per-call-site reset lists had drifted (exit-guest missed favorites; delete-account reset six) — so on a shared device, user A's food logs, recents, favorites, grocery list, and daily note survived in memory into user B's session, and the note editor could even upsert A's note text into B's account. RootView now resets all six user-owned stores in ONE place via .onChange(of: authManager.userState) — covering sign-out, account deletion, guest exit, and sign-in uniformly. onChange runs before newly inserted views' .task fetches, so fresh data loads for the new identity. The duplicated reset lists in SettingsView are deleted; five now-unused store environment properties removed. Adds StoreResetTests pinning the reset() contract for FoodLogStore, WaterStore, BodyweightStore, and FavoriteFoodStore via the existing preview-seed initializers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SearchView's .task re-runs on every tab appearance, and despite the "fetched once on first appear" comment it re-downloaded the full ~1,500-row type-ahead corpus plus the empty-state suggestions on every visit — production API logs showed 6 corpus fetches in a single session (~40-60KB each of identical static data). Guard both static catalog fetches with isEmpty checks: they load once per app session and self-heal (a failed fetch leaves the array empty, so the next appearance retries). Recents/favorites/grocery keep refreshing per appearance — they are small per-user queries that genuinely change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refreshWeek issued a single un-limited ascending query, and PostgREST silently caps every response at max_rows (1,000). A high-frequency logger (11+ entries/day) exceeds that in the 90-day window, and ascending order meant the truncated rows were the NEWEST days — the Progress chart showed zero-calorie recent days for exactly the most engaged users, with no error. Fetch in explicit 1,000-row pages (hard stop at 5 pages ≈ 55 logs/day over 90 days) with a secondary id ordering so pagination is stable across rows sharing a logged_at timestamp. Typical users still make exactly one request; the 7- and 30-day ranges are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Updates to Preview Branch (audit/codebase-security-stability) ↗︎
Tasks are run on every commit but only new migration files are pushed.
View logs for this Workflow Run ↗︎. |
There was a problem hiding this comment.
Pull request overview
This PR is a broad security/stability hardening pass across the iOS app and Supabase tooling, adding stronger auth/account-deletion controls, safer client-side state handling across identity changes, improved operational checks (production drift), and expanded regression tests to lock the behaviors in.
Changes:
- Strengthens auth/account security (password policy tightening, password recovery UI flow, and Apple reauthorization + server-side revocation before Apple-backed account deletion).
- Prevents cross-user data bleed by centralizing user-owned store resets on identity changes and guarding stores against stale async updates.
- Adds production Supabase drift detection (script + workflow + documentation) and removes tracked generated
graphify-outartifacts.
Reviewed changes
Copilot reviewed 80 out of 97 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| supabase/functions/delete-account/index.ts | Refactors Edge Function entrypoint to delegate to a dedicated handler. |
| supabase/config.toml | Tightens auth settings (password policy, secure password change, JWT verification for delete-account). |
| scripts/supabase/check-production-drift.sh | Adds a read-only production DB drift check runner using psql. |
| docs/supabase-production-drift-check.md | Documents drift check scope, safe usage, and GitHub Actions setup. |
| .github/workflows/supabase-production-drift.yml | Adds scheduled/manual workflow to run production drift checks. |
| .github/workflows/ci.yml | Adds Deno setup + Edge Function tests for delete-account. |
| .github/workflows/claude.yml | Hardens Claude workflow triggering/permissions and pins the action SHA. |
| .github/workflows/claude-code-review.yml | Hardens Claude review workflow triggering/permissions and pins the action SHA. |
| AkFit/AkFit/AkFitApp.swift | Adds URL handling for recovery links and centralizes user-owned store resets on identity transitions. |
| AkFit/AkFit/Views/Auth/AuthView.swift | Updates password-reset UX copy and applies stricter password policy on sign-up only. |
| AkFit/AkFit/Views/Auth/SetNewPasswordView.swift | Adds password recovery “set new password” screen + validation policy. |
| AkFit/AkFit/Views/Settings/SettingsView.swift | Adds Apple reauth sheet for account deletion and routes deletion through Apple code when required. |
| AkFit/AkFit/Views/Search/SearchView.swift | Reduces refetch storms and improves grocery action UX/error handling + disables interactions while busy. |
| AkFit/AkFit/Logging/FoodLogStore.swift | Adds identity-safety guards for async refresh/write paths and implements paged history loading. |
| AkFit/AkFit/Logging/WaterStore.swift | Adds identity-safety guards to prevent stale async writes/refreshes after sign-out/account switch. |
| AkFit/AkFit/Logging/BodyweightStore.swift | Adds identity-safety guards to prevent stale async writes/refreshes after sign-out/account switch. |
| AkFit/AkFit/Logging/DailyNoteStore.swift | Adds identity-safety guards to prevent stale async writes/refreshes after sign-out/account switch. |
| AkFit/AkFit/Logging/FavoriteFoodStore.swift | Adds identity-safety guards and safer optimistic update reverts across identity changes. |
| AkFit/Config/AppConfig.swift | Adds stricter Supabase URL validation to prevent unsafe/misconfigured endpoints. |
| AkFit/Services/SentryMonitoring.swift | Disables network telemetry capture and scrubs URLs/headers/breadcrumb data before sending. |
| AkFit/AkFit/Models/FoodLog.swift | Adds resilient decoding for legacy guest-cached rows missing/invalid meal_slot. |
| AkFit/AkFit/PrivacyInfo.xcprivacy | Adds required Apple privacy manifest for app-level required-reason APIs. |
| AkFit/AkFitTests/StoreResetTests.swift | Adds tests pinning store reset semantics across identity transitions. |
| AkFit/AkFitTests/SentryMonitoringTests.swift | Adds tests for URL/header/data scrubbing helpers. |
| AkFit/AkFitTests/PasswordRecoveryTests.swift | Adds tests for password recovery validation + AuthManager recovery event behavior. |
| AkFit/AkFitTests/GuestDataStorePersistenceTests.swift | Adds tests for schema-evolution-safe guest persistence and protected storage behavior. |
| AkFit/AkFitTests/AuthManagerDeleteAccountTests.swift | Adds tests around Apple deletion requirements and debug-log sanitization. |
| AkFit/AkFitTests/AppConfigTests.swift | Adds tests for Supabase URL validation rules. |
| .gitignore | Ignores graphify-out/ going forward. |
| graphify-out/GRAPH_REPORT.md | Removes tracked generated graph report output. |
| graphify-out/cache/e11fa65adcaae970332a0cfff21709f944a4beeac95c8264712977c85c1a3d74.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/d204ea1fa8d2e3260a1c59e2a864d1978ad319fa8c988348c370b51200643e27.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/d0f479c4e1c4887a96c198f484c3ce796f922d70c8cb1bae86121c0d43220446.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/c75ad8a6db5b094141492703a10f1ffb8667a7508466dcde7a913e99146dc1f4.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/bffe27fb9b4a54f4777c175ecc2898dd0ef64557475f9964360f034a72e130b2.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/bcbee6f8b09fc5c643bd4d073a69ef8996759cbd5df34b917626cc25f0921b63.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/bbcc10bed2283efdd5db16631c2bac0801cde91bcc96302d8c08af1f8e50484f.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/baa63c3cf5c98a3cdbbaf90a8b5568b09f3b4cf8dfcd7385e8112c0d6c0a5ff2.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/ba8b7b4adb7d5656d2d44a577a41d3a0f6939eff04a3ed9223eadeaaf3fd6db3.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/b81ac998449d463c29f07820998035a7319a2607e34e005679f9e94793f0f5c9.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/ab24c29c9794dd5f12999b3d7606d10c4f53f1a4c994a40d63c1496127aefe99.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/96fea7b4a46de6d11edd0d888e4b57bf8ba779dc7a92c6e231341dc845b51b19.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/953ce43ded2c5fb40da203251c63f8ab2b621d82219cbc1b850ebc0027c5055c.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/8893e5e024709e977f983965b9c1be310cbfad981e8db3e441f1358af8954c42.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/7fe68d8ebebceece50cd78a1d15400df5ce449e9de6275d20d56a088dc09d753.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/75a964f6d4a66d50ab637d6b373b726458dc30ebf591d0b3f63c14a839696fbb.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/71e326c886afe22b4d709f70541f5cb12149b0d8b083a8108b4b4fb3882dd764.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/6e3ca76195f6db34b60e487d13527ad9bb3b227798f563fe67af34b4c74cfb5e.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/544516b1b6df806e43cdc4277c0deebd9984030687949eff01966b56c308b73c.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/4b52614bc573fa6daadff4935cbba3729743edc202f38ce85ac3e37c0eafdbab.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/1f6eefc480fc455029da65735b9d9d49ccf2a130cb3dee7d7228ae770ea70eff.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/17d4b182f0b0dc232ea38bbe3158ba77d8593891aa3020953b99f995d8f6c534.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/07f05f75c08be8c3f88566a86d62b3f7e57ceafed67c5c2d3e8c675fc7ca89a1.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/38ef77636049c30d2f10a2a5a4da99630f5be25170e7651befd66fb9f5d5a40d.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/367315176110df03d3c529c3b1e1d0fe86ebaea6dbf1ec0539a559ba7ecd8894.json | Removes tracked generated graph cache artifact. |
| graphify-out/cache/3200686a9ff2436c08cdf38f4d58daba30602fcc79a705c918b479666ac889df.json | Removes tracked generated graph cache artifact. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| loggedAt = try c.decode(Date.self, forKey: .loggedAt) | ||
| createdAt = try c.decode(Date.self, forKey: .createdAt) | ||
|
|
||
| let rawMealSlot = (try? c.decodeIfPresent(String.self, forKey: .mealSlot)) ?? nil |
| let pageSize = 1_000 | ||
| let maxPages = 5 // hard stop ≈ 55 logs/day over 90 days | ||
| for page in 0..<maxPages { |
There was a problem hiding this comment.
💡 Codex Review
AkFit-iOS/AkFit/AkFit/Auth/AuthManager.swift
Lines 425 to 427 in 0bf3a41
When auth.signOut() throws, this fallback only clears AuthManager's in-memory state; it does not clear the Supabase client's persisted session/refresh token. In the network-failure or revoke-failure path, the UI routes to signed-out state for this run, but the next auth observer/launch can restore the still-stored session and sign the user back in. Use a local sign-out/session removal path in this catch before clearing app state.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| try await SupabaseClientProvider.shared.auth.resetPasswordForEmail( | ||
| email, | ||
| redirectTo: PasswordRecoveryLink.redirectURL(state: state) | ||
| ) |
There was a problem hiding this comment.
Add the recovery callback to Supabase redirects
This reset flow now always asks Supabase to send users to akfit://auth-callback?..., but the checked-in Supabase auth config still only allow-lists https://127.0.0.1:3000 under [auth].additional_redirect_urls. In environments managed from this config, resetPasswordForEmail will reject or strip the custom callback, so email/password users still cannot complete password recovery until the akfit://auth-callback redirect is allowed.
Useful? React with 👍 / 👎.
* Post-merge release-safety review: fix P1 silent note-save failure + hygiene Multi-agent review (7 dimensions, adversarially verified) of the merged security/stability work found no P0s and two P1s, both fixed here: - DailyNoteStore.save no longer swallows persistence failures behind a false success signal: it returns Bool, mutates todayContent only after the write is decided, logs + captures failures to Sentry with standard classification tags, and NoteEditorSheet keeps the sheet open with a retry alert (swipe-dismiss blocked mid-save so typed text can't be destroyed by a failing in-flight save). - StoreResetTests now pins reset() for all six user-owned stores (DailyNoteStore and GroceryListStore were unpinned — the exact cross-user data-leak regression class PR #28 fixed). Tiny verified P2 hardening included: - SearchView post-log Undo surfaces delete failures (alert + Sentry) instead of silently leaving the entry logged. - FoodDetailView favorite toggle captures errors to Sentry (was the only unobserved user-initiated Supabase write). - .gitignore adds *.xcarchive / *.xcresult. - claude.yml pins actions/checkout to the v4.3.1 release SHA (secret in scope in that job). - ci.yml also runs on push to main so direct pushes can't bypass tests. - config.toml: akfit://auth-callback added to the redirect allow-list (exact entry for OAuth + ** wildcard for query-string recovery links, per GoTrue glob semantics) and documents the dashboard-managed production auth posture (email confirmations, leaked-password protection). - Stale pre-PR#28 reset() comments in four stores corrected; README and release checklist note that CLAUDE.md / docs/ui-reference are local-only. Verified: 119 tests in 18 suites pass on iPhone 17 Pro simulator; edited TOML/YAML parse clean; production Supabase advisors checked live (only pre-existing WARNs: pg_trgm in public, leaked-password protection off). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X3aqVLXYVueZBeKd4U7imw * Address PR review: clarify save() identity semantics, add auth_code tags - DailyNoteStore.save doc: identity change BEFORE the write returns false; identity change AFTER a successful upsert returns true (the write persisted for the original account; only the local mutation is skipped, RootView resets the store). Code behavior unchanged — the comment was imprecise (Copilot review). - Add the auth_code Sentry tag to the quick_log_undo and favorite_toggle captures, matching the richer grocery/daily-note tag idiom — session expiry is exactly the failure class these paths can hit (Greptile P2). 119/119 tests pass locally after the change.
This PR adds a comprehensive AkFit security and stability audit and implements focused fixes from the audit.
Includes:
Manual checks before App Store release:
Greptile Summary
This PR is a comprehensive security and stability audit implementation, touching auth flows, store lifecycle, edge functions, CI hardening, and guest data resilience. The changes are well-scoped and clearly motivated by the accompanying audit document.
RootViewonChange(of: currentUserId)hook; adds a full password-recovery state machine with deep-link validation and cancel/dismiss flows; adds Apple credential revocation with identity binding before account deletion.GroceryListStoregains a version-tracked merge strategy to prevent stale async fetches from overwriting in-flight writes;FoodLogStoreadds paginatedrefreshWeekto avoid silent row truncation for high-frequency loggers;GuestDataStoreadopts element-level lossy decoding for arrays.Confidence Score: 5/5
Safe to merge; no findings affect correctness of the critical account-deletion or password-recovery paths.
The two flagged items are both non-blocking: the redundant
setAuthcall inresolveDeleteAccountSessionleaves stale state on the shared functions client but does not affect the current deletion flow (the explicitAuthorizationheader takes precedence), and the doubleapplyAuthenticatedSessionduring recovery-URL handling is idempotent. All security-sensitive operations — Apple grant revocation with identity binding, session validation before deletion, stale-fetch guards after sign-out — are correctly implemented.AkFit/AkFit/Auth/AuthManager.swift — the redundant
setAuthcall and the dualapplyAuthenticatedSessionpath during password recovery are worth a second look before adding new edge functions.Important Files Changed
setAuthcall inresolveDeleteAccountSessionmutates global functions client state unnecessarily; and the URL handler and auth observer both callapplyAuthenticatedSessionfor the same recovery session.onChange(of: authManager.currentUserId)hook inRootView, eliminating the previous drift where individual call sites missed stores. Password recovery routing added as the first priority in theRootViewswitch.activeFetchVersions/recentlyConfirmedWritesmerge logic, optimistic toggle with revert on failure, session pre-flight before writes, and areset()method for the centralized identity-change hook.refreshWeekto avoid silent row truncation at the 1,000-row PostgREST cap for high-frequency loggers,canApplyUserOwnedStateguards after every async boundary, and areset()method.loadLossyArray(element-level resilience) andloadStringDictionary(partial-dictionary recovery), protecting against schema-evolution data loss on upgrade.actions/checkoutremains unpinned at@v4.checkoutaction pinned to commit SHA, consistent with the PR's SHA-pinning theme.authManager.requiresAppleReauthorizationForAccountDeletion; removed per-store manual reset calls (now centralized in RootView).Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant User participant iOS as iOS App participant AM as AuthManager participant RV as RootView participant SB as Supabase Auth participant EF as delete-account Edge Fn participant Apple as Apple ID note over User,Apple: Password Recovery Flow User->>iOS: Tap recovery email link iOS->>AM: handleIncomingURL(url) AM->>AM: setPasswordRecoveryPending(true) AM->>SB: auth.session(from: url) SB-->>AM: recoveredSession + fires .passwordRecovery event par URL handler path AM->>AM: applyAuthenticatedSession(recoveredSession) AM->>AM: "passwordRecoveryState = .ready" and Auth observer path AM->>AM: handle(.passwordRecovery, session) AM->>AM: applyAuthenticatedSession(session) AM->>AM: "passwordRecoveryState = .ready" end RV->>RV: Shows SetNewPasswordView User->>iOS: Submit new password iOS->>AM: updatePasswordAfterRecovery(password) AM->>SB: auth.update(user: UserAttributes) SB-->>AM: success AM->>AM: "passwordRecoveryState = .passwordUpdated" note over User,Apple: Account Deletion Flow (Apple user) User->>iOS: Confirm delete account iOS->>AM: deleteAccount(appleAuthorizationCode) AM->>AM: resolveDeleteAccountSession() AM->>SB: auth.session + auth.user(jwt:) SB-->>AM: validSession AM->>EF: POST /delete-account (Bearer JWT + appleAuthorizationCode) EF->>SB: getUser() via anon+JWT SB-->>EF: authenticated user EF->>Apple: POST /auth/token (exchange code) Apple-->>EF: id_token + refresh_token EF->>EF: verify sub matches linked Apple identity EF->>Apple: POST /auth/revoke (refresh_token) Apple-->>EF: 200 OK EF->>SB: admin.deleteUser(user.id) SB-->>EF: success EF-->>AM: 200 success AM->>SB: auth.signOut() SB-->>AM: .signedOut event AM->>RV: "userState = .signedOut" RV->>RV: onChange(currentUserId) → resetUserOwnedStores()%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant User participant iOS as iOS App participant AM as AuthManager participant RV as RootView participant SB as Supabase Auth participant EF as delete-account Edge Fn participant Apple as Apple ID note over User,Apple: Password Recovery Flow User->>iOS: Tap recovery email link iOS->>AM: handleIncomingURL(url) AM->>AM: setPasswordRecoveryPending(true) AM->>SB: auth.session(from: url) SB-->>AM: recoveredSession + fires .passwordRecovery event par URL handler path AM->>AM: applyAuthenticatedSession(recoveredSession) AM->>AM: "passwordRecoveryState = .ready" and Auth observer path AM->>AM: handle(.passwordRecovery, session) AM->>AM: applyAuthenticatedSession(session) AM->>AM: "passwordRecoveryState = .ready" end RV->>RV: Shows SetNewPasswordView User->>iOS: Submit new password iOS->>AM: updatePasswordAfterRecovery(password) AM->>SB: auth.update(user: UserAttributes) SB-->>AM: success AM->>AM: "passwordRecoveryState = .passwordUpdated" note over User,Apple: Account Deletion Flow (Apple user) User->>iOS: Confirm delete account iOS->>AM: deleteAccount(appleAuthorizationCode) AM->>AM: resolveDeleteAccountSession() AM->>SB: auth.session + auth.user(jwt:) SB-->>AM: validSession AM->>EF: POST /delete-account (Bearer JWT + appleAuthorizationCode) EF->>SB: getUser() via anon+JWT SB-->>EF: authenticated user EF->>Apple: POST /auth/token (exchange code) Apple-->>EF: id_token + refresh_token EF->>EF: verify sub matches linked Apple identity EF->>Apple: POST /auth/revoke (refresh_token) Apple-->>EF: 200 OK EF->>SB: admin.deleteUser(user.id) SB-->>EF: success EF-->>AM: 200 success AM->>SB: auth.signOut() SB-->>AM: .signedOut event AM->>RV: "userState = .signedOut" RV->>RV: onChange(currentUserId) → resetUserOwnedStores()Comments Outside Diff (1)
supabase/functions/delete-account/handler.ts, line 960-988 (link)appleSubjectFromTokenextracts thesubclaim by base64-decoding the JWT payload, but it never verifies the ES256 signature against Apple's public keys. Theissandaudchecks are valuable but are also just decoded from the unverified payload — a tampered payload would still pass them.In practice the token arrives from
https://appleid.apple.comover TLS, so exploitation requires breaking HTTPS in transit. However, Apple's own guidelines (and JWT-security best practice) require verifying the signature before trusting any claim. For an action as irreversible as account deletion and Apple-grant revocation, adding signature verification closes this gap.The fix involves fetching Apple's public keys from
https://appleid.apple.com/auth/keysand verifying the JWT signature with the matching key (kidin the JWT header) before trustingsub. This can be done with the Web Crypto API available in Deno.Reviews (4): Last reviewed commit: "Serialize password recovery tests" | Re-trigger Greptile