Skip to content

feat: add Invalidate to recover from a revoked token - #64

Closed
jferrl wants to merge 1 commit into
mainfrom
feat/token-invalidation
Closed

jferrl wants to merge 1 commit into
mainfrom
feat/token-invalidation

Conversation

@jferrl

@jferrl jferrl commented Sep 17, 2026 •

Copy link
Copy Markdown
Owner

Summary

Expiry is not the only way a token dies. GitHub revokes an installation token when the App is suspended, when its permissions change, on an explicit revocation, and it revokes every token for an installation when the private key is rotated. A cache keyed on expiry cannot see any of that, so it keeps serving the dead credential and every request 401s until the hour is up. There is currently no recovery short of restarting the process. Invalidate discards the cached token so the next call mints a fresh one.

Changes

  • Adds an exported Invalidator interface and a package-level Invalidate(src) bool helper, saving callers a type assertion on the oauth2.TokenSource the constructors return
  • Invalidate reports false for a source with nothing to discard, so a caller can distinguish "cleared" from "nothing to clear" rather than having a no-op pass silently
  • The skew <= 0 path returns a thin wrapper instead of oauth2.ReuseTokenSource directly, since the latter exposes no way to clear its cache; invalidation now works whatever the skew
  • Documents the recovery pattern in doc.go, the README and llms.txt

Rationale

Invalidate affects only the source it is called on, because an installation token and the application JWT behind it are different credentials with different lifetimes and a 401 does not say which one failed. The wrapper on the skew <= 0 path replaces its delegate rather than reaching inside it, so oauth2's refresh timing — including its own ten-second early-expiry delta — is untouched. Test_ReuseTokenSourceWithSkew_ZeroSkewKeepsOAuth2Timing pins that boundary from both sides, and Test_Invalidate_Concurrent exercises invalidation against concurrent readers under -race, since a revocation arrives while requests are in flight.

Migration Notes

Purely additive; no existing behaviour changes. The source returned for skew <= 0 is no longer literally an oauth2.ReuseTokenSource, so code type-asserting on that concrete type would be affected — it still satisfies oauth2.TokenSource and refreshes identically.

Expiry is not the only way a token dies. GitHub revokes an installation
token when the App is suspended, when its permissions change, on an
explicit revocation, and it revokes every token for an installation when
the private key is rotated.

A cache keyed on expiry cannot see any of that. It keeps serving the dead
credential, and every request 401s, until the hour is up. There was no
recovery short of restarting the process.

Invalidate discards the cached token so the next call mints a fresh one,
exposed both as an Invalidator interface and as a package-level helper
that saves callers a type assertion on the oauth2.TokenSource the
constructors return. It reports false for a source with nothing to
discard, so a caller can tell "cleared" from "nothing to clear" rather
than having a no-op pass silently.

The skew<=0 path previously returned oauth2.ReuseTokenSource directly,
which exposes no way to clear its cache. It now returns a thin wrapper
that replaces the delegate on Invalidate, so invalidation works whatever
the skew. Refresh timing is unchanged, including oauth2's own ten-second
early-expiry delta, and a test pins that boundary from both sides.
@codecov

codecov Bot commented Sep 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.08%. Comparing base (2a37180) to head (1bb41ef).

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #64      +/-   ##
==========================================
+ Coverage   96.94%   97.08%   +0.13%     
==========================================
  Files           4        4              
  Lines         295      309      +14     
==========================================
+ Hits          286      300      +14     
  Misses          9        9              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jferrl jferrl closed this Sep 18, 2026
@jferrl
jferrl deleted the feat/token-invalidation branch September 18, 2026 09:01
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