Skip to content

fix(auth): clear stale tokens after refresh failure to prevent sign-in loop#7

Merged
sirily11 merged 3 commits into
mainfrom
copilot/fix-refresh-token-logout-issue
Feb 24, 2026
Merged

fix(auth): clear stale tokens after refresh failure to prevent sign-in loop#7
sirily11 merged 3 commits into
mainfrom
copilot/fix-refresh-token-logout-issue

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 24, 2026

After a refresh token request returns 400, logout()clearAll() can fail partially (e.g., KeychainTokenStorage.clearAll() short-circuits on first delete error), leaving stale tokens in storage. Subsequent checkExistingAuth() calls find the stale refresh token, retry the failed refresh, and loop — user is stuck on the sign-in page.

Changes

  • logout() fallback cleanup: When clearAll() throws, individually attempt deleteAccessToken() and deleteRefreshToken() so a partial Keychain failure doesn't leave tokens behind
  • checkExistingAuth() catch block: Clear tokens explicitly after a failed refresh, breaking the retry loop regardless of whether handleTokenRefreshFailure() succeeded internally
  • checkExistingAuth() missing timer start: Added startTokenRefreshTimer() after successful token refresh (was present in the valid-access-token path but missing in the refresh path)

Tests

Added OAuthManagerTokenCleanupTests covering:

  • logout() clears tokens even when clearAll() throws (using a ClearAllFailingTokenStorage mock)
  • checkExistingAuth() clears stale tokens after refresh 400 (via MockURLProtocol)
  • No retry loop: second checkExistingAuth() call makes zero network requests
  • Full cycle: refresh failure → cleanup → new tokens → successful re-authentication

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@autopilot-project-manager autopilot-project-manager Bot added bug Something isn't working enhancement New feature or request labels Feb 24, 2026
…up tests

- In logout(), try individual token deletions when clearAll() throws to
  prevent partial failures from leaving stale tokens behind
- In checkExistingAuth(), clear tokens in the refresh failure catch block
  so subsequent calls don't retry a failed refresh (breaking the loop)
- In checkExistingAuth(), start refresh timer after successful token refresh
  (was missing, unlike the valid-access-token path)
- Add comprehensive tests for token cleanup after refresh 400 failure

Co-authored-by: sirily11 <32106111+sirily11@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix refresh token issue causing logout on sign in Fix stale tokens persisting after refresh 400, causing sign-in loop Feb 24, 2026
Copilot AI requested a review from sirily11 February 24, 2026 17:58
@sirily11 sirily11 marked this pull request as ready for review February 24, 2026 17:59
Copilot AI review requested due to automatic review settings February 24, 2026 17:59
@autopilot-project-manager autopilot-project-manager Bot changed the title Fix stale tokens persisting after refresh 400, causing sign-in loop fix(auth): clear stale tokens after refresh failure to prevent sign-in loop Feb 24, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Addresses an auth “sign-in loop” where stale refresh tokens can persist after a token refresh failure, causing repeated refresh attempts on subsequent session checks.

Changes:

  • Add a logout() fallback path to delete tokens individually when clearAll() throws.
  • Ensure checkExistingAuth() clears tokens after a failed refresh attempt to avoid retries.
  • Start the token refresh timer after successfully refreshing from an existing session.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
Sources/RxAuthSwift/OAuthManager.swift Adds fallback token cleanup on logout, clears tokens on refresh failure, and starts refresh timer after refresh-path restoration.
Tests/RxAuthSwiftTests/OAuthManagerTokenCleanupTests.swift Adds test coverage for logout cleanup fallback and for breaking the refresh-failure retry loop.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +80 to +82
private final class MockURLProtocol: URLProtocol, @unchecked Sendable {
nonisolated(unsafe) static var requestHandler: (@Sendable (URLRequest) -> (HTTPURLResponse, Data))?

Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MockURLProtocol.requestHandler is a global static that isn't reset between tests. Even though you unregister the protocol class, leaving the handler set can create hidden coupling/flakiness if future tests register the protocol without setting a handler, or if tests run in parallel. Consider clearing requestHandler (e.g., defer { MockURLProtocol.requestHandler = nil }) or wrapping register/unregister + handler setup in a helper to ensure isolation.

Copilot uses AI. Check for mistakes.
Comment on lines +54 to 57
// Ensure stale tokens are fully cleared so that subsequent
// calls to checkExistingAuth() don't retry a failed refresh.
try? tokenStorage.clearAll()
authState = .unauthenticated
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the refresh-token path, the catch block uses try? tokenStorage.clearAll(). Since KeychainTokenStorage.clearAll() can throw before deleting the refresh token (it short-circuits on the first delete error), ignoring the error here can still leave a stale refresh token behind and allow checkExistingAuth() to retry/loop. Consider reusing the same best-effort cleanup as logout() (attempt individual deletions when clearAll() fails), or call await logout() here to guarantee refresh-token removal.

Suggested change
// Ensure stale tokens are fully cleared so that subsequent
// calls to checkExistingAuth() don't retry a failed refresh.
try? tokenStorage.clearAll()
authState = .unauthenticated
// Use the same best-effort cleanup as logout() to ensure
// the refresh token is removed and avoid repeated retries.
await logout()

Copilot uses AI. Check for mistakes.
// Attempt to clear individual tokens even if clearAll() fails,
// so that a partial failure doesn't leave stale tokens behind.
try? tokenStorage.deleteAccessToken()
try? tokenStorage.deleteRefreshToken()
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logout()'s fallback cleanup deletes the access/refresh tokens, but it never removes the stored expiration value (KeychainTokenStorage stores it under a separate expires_at key). If clearAll() failed after partially deleting tokens, this can leave a stale expiresAt entry in Keychain. A low-impact fix is to retry clearAll() after the individual deletions (so expires_at gets cleaned up when possible), or extend the protocol with a dedicated expiresAt deletion method.

Suggested change
try? tokenStorage.deleteRefreshToken()
try? tokenStorage.deleteRefreshToken()
// Retry clearAll() to clean up any remaining metadata keys (e.g., expires_at).
try? tokenStorage.clearAll()

Copilot uses AI. Check for mistakes.
Co-authored-by: sirily11 <32106111+sirily11@users.noreply.github.com>
@sirily11 sirily11 force-pushed the copilot/fix-refresh-token-logout-issue branch from fbd401c to d347483 Compare February 24, 2026 18:42
@sirily11 sirily11 enabled auto-merge (squash) February 24, 2026 18:42
@sirily11 sirily11 merged commit ac6dbbb into main Feb 24, 2026
5 checks passed
@sirily11 sirily11 deleted the copilot/fix-refresh-token-logout-issue branch February 24, 2026 18:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants