Don't wipe local data on a transient 401 during session/request (#137)#168
Don't wipe local data on a transient 401 during session/request (#137)#168MiMoHo wants to merge 1 commit into
Conversation
|
Hello, Have you been able to validate the diagnosis made by the AI? Is there any documentation that explains which situations can produce a 401 code? In that case, it is possible that by reading the body the reason can be determined accurately? I don't like the idea of keeping a cached session when the token has been revoked, so I think we should be able to tell the situations appart. Once we can do that, I totally agree that a transient 401 error should not erase all the data. I will also take a look at this when I have more time, to see if I can find any more info. Thank you! |
…cre#137) The initial session/request threw ClientDeauthorizedException on both 401 and 403. That exception (unlike a Result.Error, which openSession handles gracefully by falling back to the cached keychain) propagates to a full logout that clears the credentials and databases and restarts the app. A 401 is not necessarily a permanent deauthorization, and it is ambiguous: verified against a live server, /session/request returns 401 with an empty body for a wrong, expired or server-side-revoked token alike, so a transient 401 cannot be told apart from a real revocation. Nextcloud's own brute-force protection surfaces as 429 (already handled here as a non-destructive API_BAD_RESPONSE), not 401; a destructive 401 comes from a server-side token invalidation or from a 401/403 injected by a captive portal / proxy / WAF in front of Nextcloud on a flaky connection, matching the hegocre#137 reports. Destroying the locally cached, E2E-encrypted vault over that is the wrong default. Now a 401 is classified as a recoverable auth error (the caller keeps the user logged in and uses the cached keychain); only an explicit 403 (the Passwords app's login-attempt lockout) deauthorizes. The classification is extracted into a pure, unit-tested function. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5ab243e to
84ddcd3
Compare
|
I validated this against a live Nextcloud + Passwords server and the server source, so I can now answer the "which situations produce a 401 / can the body tell them apart" question precisely. What
The key point for your concern: I created an app token, confirmed it works (200), revoked it, and requested again — a revoked token returns 401 with an empty body, byte-for-byte identical to a wrong password. So the status code and the body cannot distinguish a revoked token from a transient failure; there is no reliable signal to tell them apart at this endpoint. I also need to correct my own earlier wording: Nextcloud brute-force protection does not return 401 for valid credentials. Below the limit it only adds a delay (valid creds still return 200); at the limit it returns 429 ( Given that a 401 is genuinely ambiguous, not wiping the local (E2E-encrypted, offline) vault on a 401 is the safer default. I've updated the PR description and the code comment to reflect the accurate 429-vs-401 mechanism. (Disclosure: this investigation and the code were done with AI assistance and verified by me against a live server and the source.) |
Summary
Addresses #137 (app logs itself out after a while and disables autofill).
Root cause
SessionApi.requestSession()threwClientDeauthorizedExceptionon both 401 and 403:Unlike a
Result.Error— whichApiController.openSession()already handles gracefully by falling back to the cached keychain and keeping the user logged in — this exception propagates up toPasswordsViewModel(_clientDeauthorized), which triggers a full logout: the password and folder databases, the stored credentials and the cached keychain are all cleared and the app restarts to the login screen.A 401 is not necessarily a permanent deauthorization, and it is ambiguous. Verified against a live Nextcloud + Passwords server,
GET /session/requestreturns:{}/ challenge{"message":""}{"message":"Current user is not logged in"}{"message":"Login not allowed"}A revoked app token returns exactly the same
401 {"message":""}as a wrong password, so a 401 cannot be told apart from a transient failure by status code or body. Meanwhile Nextcloud's own brute-force protection surfaces as 429 (already handled here as a non-destructiveAPI_BAD_RESPONSE), not 401. The destructive 401 therefore comes from a token invalidated server-side, or from a 401/403 injected by a captive portal / reverse proxy / WAF in front of Nextcloud on a flaky connection — which matches the #137 reports (recurs "after some time", on spotty mobile data, while the server had not revoked the session).Change
requestSession()now classifies the response and only an explicit 403 (the Passwords app's deliberate login-attempt lockout) deauthorizes; a 401 is a recoverable auth error that returnsResult.Error(API_AUTH_ERROR), so the existing cached-keychain fallback keeps the user logged in and the local vault intact. The classification is a pure function:Files:
SessionApi.kt(classifier + use it),Error.kt(API_AUTH_ERROR),ApiController.kt(log the new code alongside the existing ones).Testing
SessionRequestClassificationTest): 200→SUCCESS, 401→RECOVERABLE_AUTH_ERROR, 403→DEAUTHORIZED, other→BAD_RESPONSE. Green.401 {"message":""}).Trade-off worth your call
A genuinely revoked app password (also a 401) no longer auto-logs-out; the user keeps the cached, E2E-encrypted vault with a session error and logs out manually. For a password manager I think not destroying the local vault on an ambiguous 401 is the safer default, but an alternative is to retry a few times and only then deauthorize. Happy to adjust.
Note
This contribution was made with AI assistance (Claude). All generated code was proofread and validated by the committer against a live server and the source.