Skip to content

fix(auth): only end a session when the gateway rejects the credentials - #27

Open
spacedevin wants to merge 1 commit into
mainfrom
fix/no-signout-on-transient-failure
Open

spacedevin wants to merge 1 commit into
mainfrom
fix/no-signout-on-transient-failure

Conversation

@spacedevin

Copy link
Copy Markdown
Member

Reported as "I am constantly having to relog."

Cause

refreshAccessToken and getUserInfo failures both cleared token storage unconditionally. So a request that never reached the gateway — offline, DNS, a connection reset, a sleeping laptop — or one the gateway answered with a 5xx signed the user out. Neither of those says anything about whether the credentials are still valid.

The callers could not have done better with what they were given:

  • both threw a bare Error with no status attached, so "the refresh token is revoked" and "the wifi dropped" arrived identical;
  • the error path did await response.json() on the failure body, which itself throws on a non-JSON response (an HTML 502 page from a proxy), masking the status entirely.

Fix

  • AuthError carries the HTTP status and the OAuth error code, and is thrown only when the gateway actually answered. A network rejection stays a network rejection.
  • isCredentialFailure() is true for 400 / 401 / 403 and invalid_grant / invalid_token / unauthorized_client; false for 5xx, 429 and every network rejection.
  • The provider clears storage only on a credential failure. On a transient one it keeps the tokens, and loadUser falls back to the cached user — so an offline page load no longer looks like a sign-out.

This is the narrow reading on purpose: anything the gateway did not explicitly reject is treated as "try again later", because the cost of being wrong in that direction is a retry, and the cost of being wrong in the other direction is the bug being reported.

Verification

74 pass / 0 fail across 11 files, including a new src/lib/__tests__/credential-failure.test.ts covering each status and error code, non-JSON error bodies, and network rejections.

docs/ADVANCED.md documents AuthError and the retained-session behaviour.

Refresh and user-load failures both cleared token storage unconditionally, so a
request that never reached the gateway — offline, DNS, a connection reset — or one
it answered with a 5xx signed the user out and made them log in again. Neither
says anything about whether the credentials are still valid.

The callers could not do better: refreshAccessToken and getUserInfo threw a bare
Error with no status, and the `await response.json()` in the error path threw on a
non-JSON body (an HTML 502 page), masking the status entirely.

- AuthError carries the HTTP status and the OAuth error code, and is thrown only
  when the gateway actually answered.
- isCredentialFailure() is true for 400/401/403 and invalid_grant / invalid_token /
  unauthorized_client, and false for 5xx, 429 and every network rejection.
- The provider clears storage only on a credential failure. On a transient one it
  keeps the tokens, and loadUser falls back to the cached user so an offline page
  load no longer looks like a sign-out.
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