Skip to content

fix(android-sdk): discard in-flight token responses after sign-out#263

Merged
xiaoyijun merged 4 commits into
masterfrom
xiaoyijun-fix-sign-out-refresh-race
Jun 12, 2026
Merged

fix(android-sdk): discard in-flight token responses after sign-out#263
xiaoyijun merged 4 commits into
masterfrom
xiaoyijun-fix-sign-out-refresh-race

Conversation

@xiaoyijun

Copy link
Copy Markdown
Collaborator

Summary

Fixes #253.

signOut() / clearCredentials() cleared the local credentials synchronously, but a token refresh (or sign-in code exchange) that was already in flight would land afterwards and verifyAndSaveTokenResponse would unconditionally write the rotated tokens back into memory and persistent storage — silently resurrecting a signed-out session (isAuthenticated flips back to true on the next client construction). See the issue for the deterministic repro and timeline.

This introduces a SessionGuard — an optimistic lock (the in-memory equivalent of UPDATE ... WHERE version = ?) around the credential set:

  • Async token flows (signIn, the refresh path of getAccessToken) take a stamp() of the current credential generation when they start.
  • signOut / clearCredentials drop the credentials inside invalidate {}, which bumps the generation atomically with the clear (extracted as dropCredentials()).
  • verifyAndSaveTokenResponse persists the response inside commit(stamp) {}, which applies the write only when the generation is unchanged; otherwise the response is discarded and the flow completes with NOT_AUTHENTICATED.

Clear and write are mutually exclusive critical sections (the guard's monitor), so there is no check-then-write window. A monotonic generation — rather than a boolean or an isAuthenticated re-check — also covers the "sign out, then immediately sign in again" case: a pre-sign-out response can no longer clobber the newer session with its already-revoked token family. JWT verification and all completions stay outside the lock; the critical sections contain only field writes.

No public API changes; NOT_AUTHENTICATED is reused instead of adding an exception type, so the fix backports to v2.x as a clean cherry-pick.

The guard is per-instance, matching the SDK's intended single-client usage; cross-instance in-memory state coherence is a pre-existing, separate concern.

Testing

unit tests — three new race tests (deterministic, captured-callback timing, no sleeps):

  1. a refresh response landing after signOut is discarded and does not resurrect the credentials;
  2. a stale pre-sign-out refresh response does not clobber the session of a later sign-in (pins the generation semantics);
  3. a sign-in code exchange resolving after signOut is discarded.

All three fail on the previous implementation and pass with the fix; the 35 existing tests are unaffected.

Checklist

  • .changeset (N/A — this repo uses release-please)
  • unit tests
  • integration tests (N/A)
  • necessary KDoc comments

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a sign-out race in the Android SDK where an in-flight token refresh (or sign-in code exchange) could persist rotated tokens after signOut() / clearCredentials(), unintentionally resurrecting a signed-out session. It introduces a per-client SessionGuard generation stamp to discard stale async token responses.

Changes:

  • Add SessionGuard and use a generation stamp to prevent stale token responses from being committed after credential invalidation.
  • Refactor signOut() / clearCredentials() to atomically clear credentials via dropCredentials() (which also bumps the generation).
  • Add deterministic unit tests covering refresh/sign-out and sign-in/sign-out races.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
android-sdk/android/src/main/kotlin/io/logto/sdk/android/LogtoClient.kt Introduces SessionGuard, stamps token flows, and commits token writes only if the session generation is unchanged.
android-sdk/android/src/test/kotlin/io/logto/sdk/android/LogtoClientTest.kt Adds race-focused unit tests to ensure stale in-flight refresh/sign-in results are discarded after sign-out.

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

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

@xiaoyijun xiaoyijun merged commit 6a84601 into master Jun 12, 2026
4 checks passed
@xiaoyijun xiaoyijun deleted the xiaoyijun-fix-sign-out-refresh-race branch June 12, 2026 00:17
xiaoyijun added a commit that referenced this pull request Jun 12, 2026
)

* fix(android-sdk): discard in-flight token responses after sign-out (#263)

* fix(android-sdk): discard in-flight token responses after sign-out

* fix(android-sdk): run in-flight token flows on credential snapshots

* fix(android-sdk): complete stale token flows with NOT_AUTHENTICATED consistently

* refactor(android-sdk): rename SessionGuard to CredentialGuard

(cherry picked from commit 6a84601)

* fix(android-sdk): invalidate unauthenticated sign-out flows

* fix(android-sdk): keep the refresh token when a refresh response omits it
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

bug: in-flight token refresh resurrects credentials after signOut (sign-out / refresh race)

3 participants