Skip to content

Keep refreshed token in memory when keychain write fails - #259

Open
onevcat wants to merge 5 commits into
masterfrom
feature/keychain-persist-resilience
Open

Keep refreshed token in memory when keychain write fails#259
onevcat wants to merge 5 commits into
masterfrom
feature/keychain-persist-resilience

Conversation

@onevcat

@onevcat onevcat commented Jul 24, 2026

Copy link
Copy Markdown
Member

Background

Users reported intermittent -25291 (errSecNotAvailable) keychain errors when the SDK stores a refreshed access token. When this happens, the refreshed token is discarded and the old (already consumed) refresh token is sent on the next refresh, which can force a re-login, especially for channels with refresh token rotation enabled.

Investigation showed -25291 means the keychain daemon (securityd) is unreachable or hits an internal error. It is environmental and transient: once the daemon recovers, the same write succeeds with the stored data intact.

Changes

  • AccessTokenStore.setCurrentToken no longer throws. When the keychain write fails, the token is kept in memory with a pending flag, so the current session continues with the new token and the consumed refresh token is not re-sent. The .LineSDKAccessTokenDidUpdate notification is still posted.
  • The SDK retries the pending keychain write when the app becomes active or protected data becomes available (event-driven retry, performed off the main thread).
  • Added a public .LineSDKAccessTokenDidFailToPersist notification, posted when the initial write or a retry fails. The underlying error is in userInfo under LineSDKNotificationKey.persistingError. Also exposed to Objective-C.
  • removeCurrentAccessToken now clears the in-memory token and the pending state even when the keychain does not contain the item, so a logout cannot be resurrected by a later retry.
  • Keychain write failures are logged with the raw status, application state, and protected data availability for diagnostics.
  • Added a test seam in KeychainStore to simulate keychain write failures, and AccessTokenStoreTests covering the failure, retry, lifecycle-triggered retry, notifications, logout, and 401 auto-refresh scenarios.

Behavior change

Previously, a keychain write failure surfaced as a failure result from API.Auth.refreshAccessToken (and the login flow), even though the server-side operation had succeeded. Now these operations report success and the SDK keeps the token in memory. Apps that need visibility into storing failures (for example, for telemetry) should observe .LineSDKAccessTokenDidFailToPersist. This is documented in the CHANGELOG.

Notes

If the app process terminates before a pending write succeeds, the old token is loaded on the next launch and a re-login may still be required for channels with refresh token rotation. This residual risk cannot be eliminated on the client side.

🤖 Generated with Claude Code

onevcat and others added 5 commits July 24, 2026 13:03
When storing a new access token to the keychain fails (e.g. errSecNotAvailable
while securityd is unavailable), the token is now kept in memory instead of
being discarded, so the current session continues and the consumed refresh
token is not sent again. The SDK retries the keychain writing when the app
becomes active or protected data becomes available, and logs diagnostics
(raw status, app state, protected data availability) on failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The synchronous overrides are treated as nonisolated by the XCTest SDK on
CI's Xcode version, so calling main actor-isolated helpers from them fails
to compile. Follow the async throws pattern used by RefreshTokenPipelineTests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
In Swift 4.2 mode try? does not flatten the resulting optional (pre SE-0230),
so the expression had type AccessToken??. Use do/catch instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Token refresh and login no longer report a failure when the keychain write
fails, so apps lost the ability to observe such failures. Post a public
notification (with the underlying error in userInfo) on the initial write
failure and on each failed retry, and expose it to Objective-C as well.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The retry runs on a background queue in production, which made the
lifecycle-notification tests depend on dispatch timing and flaky on CI.
Inject the retry executor so tests run it synchronously.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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