Skip to content

fix(ios): plant Django session cookie natively so it survives cold reopen#32410

Closed
clopen-set wants to merge 3 commits into
mainfrom
do/ios-native-session-cookie
Closed

fix(ios): plant Django session cookie natively so it survives cold reopen#32410
clopen-set wants to merge 3 commits into
mainfrom
do/ios-native-session-cookie

Conversation

@clopen-set
Copy link
Copy Markdown
Collaborator

@clopen-set clopen-set commented May 28, 2026

Closing & reopening the iOS app prompted for Touch ID every time because the Django session cookie was being planted from JS via document.cookie with no Max-Age — so WKWebView treated it as a session cookie and dropped it every time the app process was killed. Every cold reopen then fell through to the Keychain biometric-recovery path.

Move cookie planting to the native side (SessionCookieInstaller, called from NativeAuthPlugin after OAuth and NativeBiometricPlugin after Keychain recovery). The native write can do what document.cookie can't:

  • 2-week Max-Age (fixes the Touch ID bug)
  • HttpOnly (XSS can no longer read the session token)
  • Host-only Domain instead of .vellum.ai (no leakage to other subdomains)
  • Picks the single cookie name the backend actually reads (__Secure-sessionid on HTTPS, sessionid on LAN-IP HTTP dev)

Also deletes the 50ms waitForNativeSessionCookie polling — the native write is synchronous so the race it papered over is gone.

The settings card had to switch to a new NativeBiometric.readSessionCookie to capture the now-HttpOnly cookie when toggling biometrics on mid-session.

Test plan

On a real iPhone: log in, kill the app, reopen — should land in the authed app with no Touch ID prompt. Web Inspector should show __Secure-sessionid's Expires column as a real date ~2 weeks out (not "Session").

…open

The JS path was setting the cookie via `document.cookie` with no
Max-Age, making it a session cookie that WKWebView drops when the app
process is killed. Every cold reopen lost the cookie and fell through to
the Keychain biometric path, prompting Face/Touch ID even when the user
hadn't actually signed out.

Move cookie planting to a shared `SessionCookieInstaller` invoked from
`NativeAuthPlugin` (after the OAuth exchange) and `NativeBiometricPlugin`
(after Keychain retrieval). The native path can set `Max-Age`, `HttpOnly`,
and a host-only `Domain`, and writes directly to WKWebView's
`WKHTTPCookieStore` so there's no async flush to wait on.

Also fixes three secondary issues that came along with the JS plant:
the cookie was scoped to `.vellum.ai` (leaking the session to every
subdomain), it was not HttpOnly (readable by any XSS), and both
`sessionid` and `__Secure-sessionid` were planted on every login when
only one is read by the deployed backend.

Removes the `waitForNativeSessionCookie` polling workaround — no longer
needed because the native write completes synchronously before the
plugin promise resolves.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f39a0d6d39

ℹ️ 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".

Comment thread apps/ios/App/App/SessionCookieInstaller.swift
…ession

Codex caught a regression: once the cookie is HttpOnly, the settings
toggle's `getSessionTokenFromCookies()` call returns null, so flipping
biometrics on mid-session silently fails to store a token in the
Keychain while still flipping the preference — the next cookie expiry
then can't be recovered with Face/Touch ID.

Add `NativeBiometric.readSessionCookie({ serverURL })` which reads
the cookie out of WKWebView's jar (where the native side can see
HttpOnly cookies), and only flip the preference if storing the token
succeeded. Deletes the now-dead `getSessionTokenFromCookies` helper.
@clopen-set
Copy link
Copy Markdown
Collaborator Author

@codex review this PR again — the previous issues have been fixed in commit 0dff388

@clopen-set
Copy link
Copy Markdown
Collaborator Author

@devin review this PR again — the previous issues have been fixed in commit 0dff388

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0dff3881e6

ℹ️ 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".

Comment thread apps/ios/App/App/NativeBiometricPlugin.swift
…cookie

Codex caught that `readSessionCookie`'s matcher iterated the cookie
store via `first(where:)` with a `Set`, so on HTTPS builds with both
cookies present, store order rather than name preference decided which
one was returned. Upgraded users (whose WKWebView jars may still carry
the legacy `.vellum.ai`-scoped `sessionid` from the pre-PR dual-cookie
planting) could see the stale token end up in the Keychain when toggling
biometrics on.

Switch to a two-pass lookup: look for the preferred name first, only
fall back to bare `sessionid` if no `__Secure-` cookie is present.
@clopen-set
Copy link
Copy Markdown
Collaborator Author

@codex review this PR again — the previous issues have been fixed in commit b8d8000

@clopen-set
Copy link
Copy Markdown
Collaborator Author

@devin review this PR again — the previous issues have been fixed in commit b8d8000

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

ℹ️ 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".

@clopen-set
Copy link
Copy Markdown
Collaborator Author

Closing — this PR scoped in too much. The user-reported bug (Touch ID on every cold reopen) has a one-line fix: add max-age=1209600 to the cookie string in installSessionCookies. That makes the cookie persistent and WKWebView stops dropping it.

This PR also fixed three latent issues that came along "for free" once I moved cookie planting to native Swift: HttpOnly (defense in depth, not critical for our single-origin shell), host-only Domain (no leak to other vellum.ai subdomains), and dropping the duplicate cookie name. None of those were reported and bundling them silently was a scope-creep mistake. They each deserve their own change with their own discussion.

Will reopen a minimal max-age-only PR after local QA.

@clopen-set clopen-set closed this May 29, 2026
@clopen-set clopen-set deleted the do/ios-native-session-cookie branch May 29, 2026 02:08
@clopen-set
Copy link
Copy Markdown
Collaborator Author

Closed in favor of #32529

It's a simpler approach that preserves existing JS cookie setting flow. We should still fix the overly broad domain + cookie naming issues

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