Skip to content

fix: log in again when the auth server rejects an expired refresh token - #165

Merged
christian2denker merged 1 commit into
mainfrom
fix/expired-refresh-token-relogin
Oct 16, 2026
Merged

christian2denker merged 1 commit into
mainfrom
fix/expired-refresh-token-relogin

Conversation

@christian2denker

Copy link
Copy Markdown
Contributor

Problem

A user who had been signed in for months got HttpRequestException(401): the token is not valid: 'token expired' [401 Unauthorized] on every screen, and logging in again failed with the same error.

The ApptiveGrid auth server answers an expired refresh token with a plain-text 401, not an OAuth error JSON. openid_client therefore throws an HttpRequestException(401) instead of an OpenIdException. Since 2.3.1 (#162) only OpenIdException counted as a rejected credential; everything else is treated like a lost connection: the credential stays stored and the error propagates. So every request, and every login attempt (which goes through checkAuthentication first), refreshed the same dead credential again and failed. Because the credential lives in the keychain, not even a reinstall helps on iOS.

The in-memory branch (token present but about to expire) had the same gap: a rejected refresh kept the expired token, so all requests kept failing with 401.

Fix

  • New ApptiveGridAuthenticator.isCredentialRejected(error): OpenIdException, or an HttpRequestException with a 4xx status except 408 (timeout) and 429 (rate limit). Connection errors and 5xx stay "not a rejection", so the offline behaviour from fix: keep the credential when a token refresh fails for want of a connection #162 is unchanged.
  • Restoring the saved credential: a rejection deletes the credential and falls through to authenticate() as before; anything else rethrows.
  • Refreshing an expired token in memory: a rejection clears token and credential and, with autoAuthenticate and requestNewToken, opens the login; anything else rethrows and keeps the token.

Tests

In test/authenticator_test.dart:

  • restoring a saved credential whose refresh gets a real plain-text 401 response from the token endpoint deletes the credential and logs in
  • expired in-memory token: rejected refresh logs in again; with requestNewToken: false it only clears the token; a connection error keeps the token and throws
  • isCredentialRejected for OpenIdException, 400/401/403, 408/429/5xx and other errors

The three behaviour tests fail without the fix. All 685 tests pass, dart analyze --fatal-infos is clean, dart format unchanged, and the authenticator has no uncovered line.

🤖 Generated with Claude Code

The ApptiveGrid auth server answers an expired refresh token with a
plain-text 401 ("the token is not valid: 'token expired'"), which
openid_client throws as an HttpRequestException rather than an
OpenIdException. Since 2.3.1 only OpenIdException counted as a rejected
credential, so the dead credential was kept and every login attempt
refreshed it again and failed the same way.

Client errors from the token endpoint (except 408 and 429) now count as
a rejection too, both when restoring the saved credential and when
refreshing an expired token in memory: the token and credential are
cleared and, with autoAuthenticate, the login page opens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the p:core Changes to apptive_grid_core label Oct 16, 2026
@christian2denker
christian2denker merged commit 1bc167f into main Oct 16, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

p:core Changes to apptive_grid_core

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant