auth: scrub refresh-token slot on logout · Entire

auth: scrub refresh-token slot on logout

22b1052→main·toothbrush·1mo ago·4 files·+50 added/-7 removed

RemoveCurrentContext and RemoveAllContexts deleted only the access-token keychain slot, leaving the long-lived refresh token behind. After logout any keyring-capable process could still mint fresh access tokens from it. Delete both the access slot and its paired :refresh slot.

Adds tokenstore.RefreshService() so the ':refresh' suffix lives in one place, and extends the logout tests to assert both slots are gone.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

Sessions

660b42be1060View transcript

Changes

4


  // Best-effort keychain cleanup, sequenced off the context just removed.
  // A missing entry is fine — the contexts.json removal above is what makes
  // us "logged out".
  // us "logged out". Both the access slot and its paired refresh slot must
  // go: leaving the long-lived refresh token behind would let any later
  // keyring-capable process mint fresh access tokens after logout.  
  if svc != "" && handle != "" {  
      _ = tokenstore.Delete(svc, handle) //nolint:errcheck // best-effort; contexts.json removal is the source of truth for logout  
      _ = tokenstore.Delete(svc, handle)                            //nolint:errcheck // best-effort; contexts.json removal is the source of truth for logout  
      _ = tokenstore.Delete(tokenstore.RefreshService(svc), handle) //nolint:errcheck // best-effort; absent refresh slot is fine  
  }  
  return nil
}`

for _, c := range f.Contexts {
if c.KeychainService != "" && c.Handle != "" {
_ = tokenstore.Delete(c.KeychainService, c.Handle) //nolint:errcheck // best-effort; the contexts.json clear below is authoritative
// Delete both slots: the access token and its paired refresh
// token. Dropping the refresh slot is what actually scrubs the
// machine — otherwise a leftover refresh token outlives logout.
_ = tokenstore.Delete(c.KeychainService, c.Handle) //nolint:errcheck // best-effort; the contexts.json clear below is authoritative
_ = tokenstore.Delete(tokenstore.RefreshService(c.KeychainService), c.Handle) //nolint:errcheck // best-effort; absent refresh slot is fine
}
removed++
}


**... (truncated for brevity) ...**

// RefreshService returns the paired refresh-token service name for an
// access-token service, following the "<service>:refresh" convention.
// Callers store the raw refresh token under (RefreshService(service), user) alongside the access token at (service, user).

func RefreshService(service string) string {
    return service + ":refresh"
}