Clear session on invalid_dpop_proof during token refresh - #109
Merged
Conversation
getUserInfo() chains a userinfo request after refresh regardless of whether the refresh succeeded, so a refresh failure can be masked by a follow-up error from that request. Add a dedicated button that calls refreshAccessToken() directly so its actual result (e.g. invalid_dpop_proof) is visible for manual testing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A restored refresh token no longer matches the DPoP key (e.g. after a device backup restore where the platform key can't be restored), so the server correctly rejects refresh with invalid_dpop_proof. The SDK only cleared the session for invalid_grant, leaving sessionState stuck at authenticated and refresh retrying forever. Treat invalid_dpop_proof the same as invalid_grant. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Previously the stream event only carried a SessionStateChangeReason, so apps had no way to distinguish why a session was cleared for reason == SessionStateChangeReason.invalid (invalid_grant, invalid_dpop_proof, or a server InvalidGrant error all looked the same). Thread the underlying error through _clearSession/_setSessionState so SessionStateChangeEvent.error carries it — null for every other reason, present whenever the clear was error-driven. The example app logs it via print() rather than showing a dialog. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…proof Demonstrates how to narrow the Object? error passed to onSessionStateChange down to OAuthException and check its .error code. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
sessionState optimistically reports authenticated based on a persisted refresh token alone, before any network round trip. If that session is actually unusable (e.g. invalid_grant or invalid_dpop_proof from a mismatched DPoP key), the getUserInfo() call right after configure() throws and was previously unhandled, crashing the app instead of falling back to "not logged in". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The listener was only attached inside the final setState() block, after configure() (which fires a foundToken event) and getUserInfo() (which, on failure, fires an invalid_grant/invalid_dpop_proof-driven clear) had already run. onSessionStateChange is a broadcast stream and does not buffer events for late subscribers, so both events were silently dropped and the listener never logged anything on Configure. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
tung2744
approved these changes
Aug 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ref DEV-3680
To test, you can follow the steps in the PR description: