Skip to content

fix: persist region in secureStorage alongside OAuth tokens to prevent cross-instance auth#90

Closed
anikdhabal wants to merge 1 commit into
mainfrom
devin/1779203092-fix-region-scoped-cache
Closed

fix: persist region in secureStorage alongside OAuth tokens to prevent cross-instance auth#90
anikdhabal wants to merge 1 commit into
mainfrom
devin/1779203092-fix-region-scoped-cache

Conversation

@anikdhabal
Copy link
Copy Markdown
Contributor

@anikdhabal anikdhabal commented May 19, 2026

Summary

Fixes a critical security vulnerability where a cal.eu user could accidentally see another user's data from cal.com when both users share the same numeric user ID (82126).

Root cause: On iOS, OAuth tokens are stored in SecureStore (Keychain), which persists across app reinstalls, but the region (US/EU) is stored in AsyncStorage, which does not persist across reinstalls. After a reinstall or AsyncStorage corruption, the tokens survive while the region defaults to "us", routing all API calls to api.cal.com instead of api.cal.eu — returning a completely different user's data.

Primary fix: Store the region in secureStorage alongside the OAuth tokens (cal_auth_region key). On boot, restore the region from secureStorage before preloadRegion() so the authoritative region is never lost, regardless of AsyncStorage state.

Defense-in-depth: Scope the persisted query cache key by region (cal-companion-query-cache-us / cal-companion-query-cache-eu) and clear the in-memory query cache on region change.

Changes

  • AuthContext.tsx:
    • New AUTH_REGION_KEY stored in secureStorage alongside tokens on login (both OAuth and web session)
    • On boot, restore region from secureStorage before preloadRegion()
    • Clear AUTH_REGION_KEY on logout alongside other auth state
  • queryPersister.ts: Scope persisted cache storage key by region; clear legacy unscoped key on cache clear
  • QueryContext.tsx: Subscribe to region changes and clear in-memory query cache when region changes

Review & Testing Checklist for Human

  • Verify on iOS: Install the app → select EU → log in via cal.eu → delete and reinstall the app → confirm the app restores the EU region and routes API calls to api.cal.eu (not api.cal.com)
  • Verify region stored on login: After OAuth login, check that cal_auth_region is written to SecureStore with the correct value ("us" or "eu")
  • Verify logout clears auth region: After logout, confirm cal_auth_region is removed from SecureStore and the login screen region picker appears
  • Verify query cache isolation: Log in as a US user → check cached data → switch to EU account → confirm no US cached data is served
  • Verify web/extension still works: The extension stores both region and tokens in chrome.storage.local (same persistence), so no change in behavior expected — but verify the sidebar still loads correctly

Notes

  • This bug is limited to the same device — it requires tokens from a previous session to still be in SecureStore while AsyncStorage has been cleared
  • The query cache scoping is defense-in-depth; the primary fix is the secureStorage region persistence
  • Existing users who already have tokens but no cal_auth_region in secureStorage will fall through to the existing preloadRegion() path (backward compatible)

Link to Devin session: https://app.devin.ai/sessions/0e094b9d77cc4b6ebf523ce7ef4ef1f4
Requested by: @anikdhabal


Summary by cubic

Fixes cross-instance auth on iOS by persisting the region (US/EU) in secure storage with OAuth tokens and scoping query caches by region. Prevents users from being routed to the wrong API after app reinstalls or storage corruption.

  • Bug Fixes
    • Save region in secureStorage (cal_auth_region) when storing OAuth or web-session auth; restore it on boot before preloadRegion().
    • Clear cal_auth_region on logout with other auth keys.
    • Scope React Query persisted cache key by region and remove the legacy unscoped key.
    • Clear in-memory query cache when the region changes to avoid cross-region stale data.

Written for commit 1be585b. Summary will update on new commits. Review in cubic

…t cross-instance auth

On iOS, SecureStore (Keychain) persists across app reinstalls but AsyncStorage
does not.  When the region was only stored in AsyncStorage, a reinstall (or
AsyncStorage corruption) would lose the region while the OAuth tokens survived
in SecureStore, defaulting API calls to cal.com instead of cal.eu.  If two
users on different instances share the same numeric user ID (e.g. 82126), the
app would authenticate to the wrong instance and return the wrong user's data.

Changes:
- Store current region in secureStorage (AUTH_REGION_KEY) whenever OAuth tokens
  or web-session auth state are saved
- On boot, restore the region from secureStorage before preloadRegion() so the
  authoritative region is never lost
- Clear AUTH_REGION_KEY on logout alongside other auth state
- Scope the persisted query cache key by region as defense-in-depth
- Clear in-memory query cache on region change to prevent stale cross-region data

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>
@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@anikdhabal anikdhabal closed this May 19, 2026
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