Skip to content

feat(notifications): bootstrap an authenticated WebView session for member pages - #19

Merged
Aalv3 merged 1 commit into
fix/auth-failure-classification-20260830from
fix/native-authenticated-webview-20260909
Sep 9, 2026
Merged

Aalv3 merged 1 commit into
fix/auth-failure-classification-20260830from
fix/native-authenticated-webview-20260909

Conversation

@Aalv3

@Aalv3 Aalv3 commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Backlog #9. Implements the authenticated first_party_web path using the supported OTP contract. JS-only, native-only — no server change. Supersedes PR #18 (fallback), fixes the destination-render FAIL from PR #16.

Auth / session state flow

  1. Notification tap → marks read (unchanged) → classifyFirstPartyMemberRoute → first_party_web
  2. destinationPresentation → { kind: 'web', url }
  3. Discourse._openFirstPartyWeb → resolveWebSessionEntry:
    • Live _t cookie? → load the destination directly, no OTP minted
    • No session? → POST /user-api-key/otp (public_key, auth_redirect, padding=pkcs1) via site.jsonApi → parse redirect_url → RSA-decrypt oneTimePassword → /session/otp/<otp> with the destination remembered
  4. Member completes the existing confirmation form — never bypassed; it sets the session cookie
  5. Navigation leaves the OTP path → the originally requested destination loads once → authorization window closes
  6. Any failure or cancellation → bounded explicit state, no blank WebView

Reuse, not reimplementation

ensureRSAKeys / rsaKeys.public / decryptHelper are the same machinery as the authorization flow, and parseAuthCallbackParameters is the same redirect parser. site.jsonApi carries the existing User-Api-Key / User-Api-Client-Id headers and inherits the rate-limit buckets and cooldowns.

Security

  • The WebView guard is untouched for everything else. The relaxation requires a pending destination from an app-initiated bootstrap and closes as soon as the destination loads — not a standing "any internal page opens" rule.
  • The decrypted OTP must match the route's hex constraint (/^[0-9a-f]+$/) before path interpolation — ../../admin, abc/def and empties are refused.
  • Off-origin destinations refused before any OTP is minted.
  • Non-staff /admin, malformed payloads, unknown types, unauthenticated callers: all still denied.
  • Server-side, User API keys are refused for suspended/inactive users, so this cannot escalate.
  • Unreadable cookie jar → bootstrap rather than assume a session (worst case: one extra single-use OTP).

Tests — 96 suites / 780 tests

New webViewSession.test.js (25) covers: OTP request shape and credential reuse; RSA decrypt reuse; unauthenticated and missing-key refusal before any request; missing/off-origin redirect_url; non-hex OTP rejection; 429 propagation; bootstrap URL constraints; session reuse skipping the OTP; empty/absent/unreadable cookie handling; off-origin destination refusal; presentation mapping for all six classes; native routing unchanged; denied cases; bounded failure copy; and wiring assertions that openUrl never opens the WebView without session resolution and that the policy relaxation is bootstrap-scoped.

validate:system: format/lint clean, verify:ota 17/17, verify:ios-auth 12/12, native Release BUILD SUCCEEDED.

One pre-existing PR #16 test asserted the old route.disposition shape inside openUrl; it now asserts the refactored presentation.kind form.

Scope

js/webViewSession.js (new), js/notificationDestination.js (new), js/Discourse.js (+62/−17), js/screens/WebViewScreen.js (+1), js/screens/WebViewScreenComponents/WebViewComponent.js (+32), tests. No ios/, android/, vendor/, config, dependency or lockfile change. Runtime an-ios-android-1.0.0-native-2 unchanged; JS-only OTA feasible.

Not device-validated. No OTA published.

…ember pages

First-party member pages could not open in the app because the WebView carries
cookies, not the User API key, and WebViewComponent's navigation policy
correctly refuses to load a canonical page into an unauthenticated Discourse
session. PR #16 routed there anyway, which produced a blank screen stuck on
"Still loading...".

The supported contract closes the gap. webViewSession posts the app's existing
RSA public key to /user-api-key/otp with the governed auth_redirect and pkcs1
padding, using site.jsonApi so the existing User-Api-Key and
User-Api-Client-Id headers, rate-limit buckets and cooldowns all apply. The
returned redirect_url is parsed with the same helper the authorization callback
uses and the one-time password is decrypted with the same JSEncrypt private
key. No second cryptographic implementation, and no new server endpoint.

The WebView then loads /session/otp/<otp>, the member completes the existing
confirmation form, and only then is the originally requested destination
loaded. The confirmation step is never bypassed: it is what sets the session
cookie.

An existing session is reused rather than spending an OTP: a live Discourse _t
cookie means the destination loads directly. The cookie package is required
lazily so importing this module does not pull a native dependency into every
suite that reaches Discourse.js.

The WebView policy relaxation is scoped to a bootstrap the app itself started.
Authorization requires a pending destination, and the window closes the moment
that destination loads, so this is not a standing "any internal page opens"
rule. The original guard is untouched for everything else.

Safety holds throughout. The decrypted OTP must match the route's hex
constraint before it is interpolated into a path. Off-origin destinations are
refused before an OTP is minted. Non-staff /admin, malformed payloads, unknown
types and unauthenticated callers remain denied. Any failure or cancellation
ends in a bounded explicit state rather than a blank WebView. Native Topic and
MemberProfile routing, notification read-marking and the private member-photo
credential boundary are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fk48MTrNBBSZeLvcJmc8SR
@Aalv3
Aalv3 merged commit 335419e into fix/auth-failure-classification-20260830 Sep 9, 2026
4 checks passed
@Aalv3
Aalv3 deleted the fix/native-authenticated-webview-20260909 branch September 9, 2026 13:57
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