Skip to content

Clear session on invalid_dpop_proof during token refresh - #109

Merged
tung2744 merged 6 commits into
authgear:mainfrom
carmenlau:dev-3680-invalid-dpop-proof
Aug 20, 2026
Merged

Clear session on invalid_dpop_proof during token refresh#109
tung2744 merged 6 commits into
authgear:mainfrom
carmenlau:dev-3680-invalid-dpop-proof

Conversation

@carmenlau

Copy link
Copy Markdown
Contributor

ref DEV-3680

To test, you can follow the steps in the PR description:

carmenlau and others added 2 commits August 6, 2026 22:01
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>
carmenlau and others added 4 commits August 12, 2026 16:01
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
tung2744 merged commit a51846b into authgear:main Aug 20, 2026
5 checks passed
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.

2 participants