Conversation
The device bootstrap failed at POST /user-api-key/otp with a 4xx. UserApiKeysController#require_params_otp requires public_key, auth_redirect and application_name; application_name was omitted, so the request was rejected as ParameterMissing before any OTP was minted. Two contract corrections: - Request /user-api-key/otp.json. create_otp responds to both html and json, and site.jsonApi sends no Accept header, so without the extension the server answers format.html with a 302 to the custom scheme, which fetch cannot follow. - Send application_name, reusing siteManager.deviceName so the OTP request carries the same identity the authorization flow registers rather than inventing a second one. The failure was also undiagnosable from the device: every stage collapsed into one bounded catch behind "Not available right now". Bootstrap failures now record a coarse stage - otp_request, otp_response_parse, otp_callback_parse, otp_decrypt, otp_validation, webview_bootstrap, destination_resume - plus a status class, through the existing profileDiagnostics allowlist. No bodies, keys, tokens, OTP values or URLs are recorded. Unchanged: the RSA machinery, the User-Api-Key headers and rate-limit buckets, the confirmation form, the canonical-origin destination boundary, and the bootstrap-scoped WebView navigation window. Also fixes a real-clock race in rateLimitResilience: Date.now() was evaluated twice in one assertion and could straddle a millisecond. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fk48MTrNBBSZeLvcJmc8SR
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fk48MTrNBBSZeLvcJmc8SR
Aalv3
changed the base branch from
main
to
fix/auth-failure-classification-20260830
September 9, 2026 14:46
Owner
Author
|
Superseded by #22. The OTP request contract in this PR was correct - the request succeeded on device and the confirmation form rendered - but Finish Login failed with "Missing, invalid or expired token", and the founder has since ruled the architecture wrong regardless: a member already authenticated in the native app must not perform a second web authentication to read a notification. Notification taps now resolve to native intents. History preserved in testing/native-auth-stale-identity/BACKLOG.md item 9; the OTA provenance tag ota-eb509d26-43af7b78 remains immutable. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
PR #19's device validation failed at step A. The request ledger showed, twice:
Root cause, from
app/controllers/user_api_keys_controller.rb:application_nameis required and was omitted, so the request was rejected asActionController::ParameterMissingbefore any OTP was minted.A second contract issue was found in the same read:
create_otpusesrespond_towith both an HTML and a JSON branch, andsite.jsonApisends noAcceptheader. Without an explicit.json, the server takesformat.htmland answers 302 to the custom scheme, whichfetchcannot follow.Changes
js/webViewSession.jsOTP_ENDPOINTis/user-api-key/otp.jsonpublic_key,auth_redirect,application_name,padding—application_namereusessiteManager.deviceName, the same identity the authorization flow registersWEB_SESSION_STAGES+webSessionFailure(stage, cause)classify each failure pointjs/Discourse.js,js/screens/WebViewScreenComponents/WebViewComponent.js{event, stage, outcome, category}through the existingprofileDiagnosticsallowlist, readable on device viayarn device:harness profile-diagnostics.destination_resumerecords the terminal success stage.The dialog itself was working as designed; it was simply the only observable, which is why a 400 and a decrypt failure were indistinguishable on device.
Unchanged
RSA machinery (no second cryptographic implementation),
User-Api-Key/User-Api-Client-Idheaders, rate-limit buckets and cooldowns, the/session/otp/confirmation form, the canonical-origin destination boundary, the bootstrap-scoped WebView navigation window, and the/renaissance/member-photo/credential boundary. No new bridge endpoint. No runtime version change. JS-only.Diagnostics contain no secrets
Only a fixed stage label and a coarse status class (
429/4xx/5xx/timeout/network_or_unknown). A test asserts that no failure carries the User API key, either RSA key, the encrypted blob, the OTP, the client id, or a URL.Gates
format:checkclean,lintcleanwebViewSession.test.js)verify:ota,verify:ios-auth,verify:release-readiness,validate:otaall PASSAlso fixes a real-clock race in
rateLimitResilience.test.js:Date.now()was evaluated twice in one assertion and could straddle a millisecond. It failed once in a full-suite run and passed in isolation.Not done here
No OTA published. Basic is untouched and preserved as the second device fixture.