Skip to content

Invalidate XSTS tokens rejected before NotAfter - #37

Merged
lactyy2 merged 11 commits into
df-mc:mainfrom
HashimTheArab:agent/xsts-token-expired-retry-upstream
Aug 26, 2026
Merged

Invalidate XSTS tokens rejected before NotAfter#37
lactyy2 merged 11 commits into
df-mc:mainfrom
HashimTheArab:agent/xsts-token-expired-retry-upstream

Conversation

@HashimTheArab

@HashimTheArab HashimTheArab commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

  • detect an authenticated Xbox API 401 that explicitly reports error=token_expired
  • invalidate only the rejected XSTS token for its relying party and clear matching SISU authorization state
  • use a per-relying-party invalidation generation to prevent overlapping acquisition from restoring the rejected token
  • expose invalidation as the optional nsal.TokenInvalidator capability
  • return the original response without retrying, leaving retry policy to callers
  • accept both single-quoted and standard double-quoted token_expired challenges

The expiry detection lives in the shared NSAL transport, so all authenticated Xbox API clients can discard a rejected cached token. Callers that need immediate replay can wrap xsapi.Client.HTTPClient() with an outer retry client such as go-retryablehttp; callers without retry logic recover on their next normal request. Other 401 responses retain their existing behavior.

Root cause

XSAPI considered the cached XSTS token valid using its signed NotAfter value. Xbox nevertheless rejected it early, so subsequent requests kept reusing the same locally valid token. Refreshing only after local expiration could not recover.

Production evidence

These are sanitized observations from mcxboxbroadcast in na-east-2; tokens, user hashes, and account identifiers are omitted.

The cached token claimed another ~3.5 hours of validity:

cached XSTS NotAfter: 2026-07-15 06:45 UTC
request time:         ~2026-07-15 03:20 UTC

Xbox rejected that token on independently authenticated services:

profile.xboxlive.com -> 401 Unauthorized
WWW-Authenticate: Token realm='xboxlive.com', error='token_expired', Token realm='xboxlive.com', error='token_required'

sessiondirectory.xboxlive.com (MPSD) -> 401 Unauthorized
WWW-Authenticate: Token realm='xboxlive.com', error='token_expired', Token realm='xboxlive.com', error='token_required'

At the same time, social.xboxlive.com accepted the token with 200 OK. This demonstrated that local NotAfter validation alone is insufficient.

For the controlled recovery, the Java broadcaster cache was backed up and only its root xboxLiveXstsToken entry was removed. The same Java build then fetched fresh SISU/XSTS state and logged:

Authenticated PlayLunarMC
Creation of Xbox LIVE session was successful!

@lactyy2

lactyy2 commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

This shouldn't normally happen if Token.Valid has already validated the token.
I'd rather improve the logic in Token.Valid so we can avoid making unnecessary requests.

@HashimTheArab

Copy link
Copy Markdown
Contributor Author

Token.Valid already checks that the token is non-empty, has user claims, and is before NotAfter with the existing one-minute refresh margin. In this incident the cached token had NotAfter 06:45 UTC but Profile and MPSD returned 401 error=token_expired around 03:20 UTC—roughly 3.5 hours early—while Social still accepted the same token with 200 OK. That rules out ordinary clock/expiry skew and shows service-side invalidation that cannot be predicted from the token fields.

This path makes no additional request while Token.Valid succeeds normally. It only refreshes after an Xbox service has authoritatively rejected the token with the explicit token_expired challenge, and it retries at most once. Increasing the local margin enough to cover this sample would discard every token hours early and still would not handle arbitrary or service-specific revocation. If there is another client-observable signal available before the request, I can incorporate it, but I do not think Token.Valid alone can represent this case.

@HashimTheArab HashimTheArab changed the title Refresh XSTS tokens rejected before NotAfter Invalidate XSTS tokens rejected before NotAfter Aug 25, 2026
@lactyy2
lactyy2 merged commit 0b93a9a into df-mc:main Aug 26, 2026
1 check 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