Skip to content

fix(auth): reconcile production-shipped stale-identity fix into the live lineage - #8

Merged
Aalv3 merged 4 commits into
fix/auth-failure-classification-20260830from
fix/native-stale-identity-ephemeral-auth-20260902
Sep 3, 2026
Merged

fix(auth): reconcile production-shipped stale-identity fix into the live lineage#8
Aalv3 merged 4 commits into
fix/auth-failure-classification-20260830from
fix/native-stale-identity-ephemeral-auth-20260902

Conversation

@Aalv3

@Aalv3 Aalv3 commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Reconciles the code currently serving production into the live source lineage, so production is not running behavior that exists only in an OTA artifact and a feature branch.

What is already in production

  • Production OTA group cb5cb74b-32a4-4aa9-ba22-6be1967f4dea
  • Shipped source SHA 5b7d754fc94d450f7ce49e7f958bb0b7a95013d1
  • Runtime an-ios-android-1.0.0-native-2
  • Rollback group 70eebadf-5736-4cd6-a7db-2980a69f0494 (fdb83141879f)

Promotion was a republish of the physically certified staging group 6796074f-42ba-4793-9a69-528ec20ec62f, not a rebundle: launch-asset hashes are identical on both platforms.

Behavior

No behavior change from what shipped. This branch is a clean fast-forward on fdb83141879f (4 ahead, 0 behind). Two commits are product code, two are lane documentation.

Physically certified on an iPhone 15 Pro running the staging Ad Hoc binary, with the active bundle confirmed from the device's expo-updates database (updateId 01a064f2ce14746c89664de78912f441, the iOS update of the certified group). Signing out and starting Member sign in opened a clean credential screen and did not reuse the prior authenticated identity.

Root cause

The ephemeral browser-auth session was necessary but not sufficient. Three carriers of a previous identity survive an app delete and reinstall:

  • Keychain User API Key and RSA material outlive the app container;
  • app deletion never clears the shared Safari cookie jar;
  • js/admissionHandoff.js opens the real Safari app via Linking.openURL, where prefersEphemeralWebBrowserSession has no effect.

SiteManager.resetAuthorizationIdentity() retires all client-side carriers together, surfaced as Use a different account. It does not depend on iOS honouring an ephemeral session and never revokes a server-side credential. The ephemeral session is retained as the cheaper mechanism when honoured.

Preserved contracts

Governed AUTH_REDIRECT callback and its allowlist; one-shot nonce/client-ID binding including the server-verified /native/v1/authorization-profile exact match; canonical-origin admission check; fail-closed channel-derived environment resolution. No native source, dependency, entitlement, permission, capability or runtime change.

The staging diagnostics block is gated on Updates.channel === 'staging' and cannot render on a production binary — the production Expo.plist carries expo-channel-name: production.

Validation

validate:system on 0c04f005: format/lint clean, 90 suites / 623 tests, verify:ota 17/17, verify:ios-auth 12/12, native Release BUILD SUCCEEDED.

Notes for the reviewer

  • main is the upstream Discourse fork baseline (July, PR UX: hide keyboard accessory bar on iPhone discourse/DiscourseMobile#294) and is 150 commits behind this lineage. fix/auth-failure-classification-20260830 is the de facto Adjuster Network trunk and the branch production ships from, which is why it is the base here.
  • testing/native-auth-stale-identity/BACKLOG.md records follow-ups including the now-stale POST-SUBMISSION-HOLD.md production-group reference. No backlog item is implemented in this PR.

Aalv3 and others added 4 commits September 2, 2026 21:07
A non-ephemeral ASWebAuthenticationSession shares the system Safari data
store. Deleting the app clears the container, AsyncStorage, the client ID,
the RSA keys and the stored User API Key, but not that shared cookie jar,
so a reinstalled app presented an already-authenticated Discourse session
and bound the new User API Key to the previous account without prompting.

Physical TestFlight Build 8 production testing confirmed the auth session
reused qa_test instead of allowing cert_probe_01 to sign in (server-side
evidence commit 35d9d27).

The native bridge already accepted an ephemeral argument and applied it as
prefersEphemeralWebBrowserSession. The defect was entirely in JavaScript:
site_manager passed a hardcoded false and requestIOSAuth defaulted to false.
EPHEMERAL_AUTH_SESSION is now a module constant rather than a caller-supplied
parameter, so no call site can reintroduce a persistent session.

The adjusternetwork://auth_redirect callback contract, the one-shot
nonce/client-ID binding, the canonical-origin admission check, and the
fail-closed channel-derived environment resolution are unchanged.

OTA-shippable on runtime an-ios-android-1.0.0-native-2: the shipped Build 8
executable already exports requestAuth:callbackURLScheme:ephemeral:resolver:
rejecter: and references setPrefersEphemeralWebBrowserSession:, so the
JS/native interface is unchanged and no new binary is required.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fk48MTrNBBSZeLvcJmc8SR
POST-SUBMISSION-HOLD.md still names group 39eb1e9b-8b72-480b-99f1-f52ad6d351fc
as production. A read-only channel check on 2026-09-02 shows production now
serves group 70eebadf-5736-4cd6-a7db-2980a69f0494 at source fdb8314,
published 2026-09-01. That document is the stated rollback target during the
Apple-review freeze, so the stale value is worth correcting.

The hold document is untracked in this lineage, so the correction is recorded
in the fix lane's backlog rather than applied to it. Documentation only; it
does not affect the staged OTA, which was published from fdd5963.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fk48MTrNBBSZeLvcJmc8SR
The ephemeral browser-auth session alone did not resolve the stale-identity
failure on a physical device, and it cannot: it is not the only carrier of a
previous member identity, and the app cannot observe whether iOS honoured it.

Three carriers survive an app delete and reinstall:

- the Keychain User API Key and RSA material, which outlive the app container
  and are restored without any staleness check;
- the shared Safari cookie jar, which app deletion never clears;
- js/admissionHandoff.js, which opens the real Safari app through
  Linking.openURL, where prefersEphemeralWebBrowserSession has no effect.

SiteManager.resetAuthorizationIdentity retires all of the client-side carriers
together - browser cookies, stored User API Key, RSA material, the recorded
authorization profile and the client ID - and the sign-in screen exposes it as
"Use a different account". This does not depend on iOS honouring an ephemeral
session and it never revokes a server-side credential.

A staging-gated diagnostics block on the sign-in screen reports the active OTA
git SHA, update ID, channel and embedded-versus-remote source, plus whether a
Keychain credential survived reinstall, so bundle activation is provable in
the product instead of over USB. It also opens the canonical session endpoint
inside the same browser-auth context, which shows directly whether that
context is anonymous. The gate is the trusted OTA channel, so the block cannot
render on production.

The ephemeral session is retained: it is correct, and it is the cheaper of the
two mechanisms when iOS honours it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fk48MTrNBBSZeLvcJmc8SR
…iagnosis

Physical testing found no avatar controls in the native Edit Profile screen.
The native editor is in fact already built: NativeProfileScreen renders the
current avatar, a Change photo action and a Remove photo action, backed by
uploadProfilePhoto/removeProfilePhoto against /native/v1/profile/photo, which
delegates to Discourse's own avatar system rather than a parallel one, with
type validation, permission and error states, and immediate cross-surface
refresh through avatarAuthority.

The block is gated on the server capability card.photo.enabled, and
POST-SUBMISSION-HOLD.md deliberately keeps structured_profile_photo_enabled
false during Apple review. The controls were therefore absent because the
capability is off, not because the UI is missing.

Remaining work is the deferred server/privacy activation lane, one genuine
native gap (camera capture, library picking only today), and a web parity
audit that cannot be performed from this repository.

Recorded only. No product code changed during auth certification.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fk48MTrNBBSZeLvcJmc8SR
@Aalv3
Aalv3 merged commit 6846b9e into fix/auth-failure-classification-20260830 Sep 3, 2026
4 checks passed
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