#466

auth: drop token-status reads that race a revocation

OpenAlpovkawants to merge intofrom

Description of the Change

Problem

TokenCache is the per-request check that an API token is still active. GetStatus reads the token row from the database, then writes the result into a 5-minute cache. The write is unconditional, so a revocation that lands between the read and the write is lost: a request reads status = active; an admin revokes the token (UpdateToken writes revoked, then calls Invalidate); the in-flight request then writes active back into the cache. For the next 5 minutes every request serves the cached active, so the revoked token keeps authenticating until the entry expires. DeleteToken has the same shape. The user-status/hash check on the same auth path is not affected, because it goes through UserCache, which already guards this race.

Solution

Apply the storeMu + epoch guard that UserCache already uses. GetStatus captures epoch before the DB read and writes both cache entries (found and not-found) through a new storeIfCurrent, which stores only if epoch is unchanged. Invalidate, InvalidateUser, and InvalidateAll bump epoch under the lock. The window drops from the full 5-minute TTL to the single in-flight request.

No related issue.

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • 🛡️ Security update

Areas Affected

  • Core Services (Frontend UI/Backend API)

Testing and Verification

Test Configuration

Backend Go unit tests only; no running stack, keys, or deployment needed.

PentAGI Version: n/a (backend unit test)
Go: 1.26.5
Host OS: Linux x86_64

Test Steps

  1. cd backend && go test ./pkg/server/auth/
  2. cd backend && go test ./pkg/server/auth/ -race
  3. Revert only api_token_cache.go and re-run the new test to see it fail on the old code.

Test Results

  • go test ./pkg/server/auth/ -race passes (full package).
  • New test TestAPITokenCache_GetStatus_DoesNotCacheAReadOverlappingAnInvalidation forces a revocation between the read and the store with a gorm query callback (same technique as the existing UserCache test). It fails on the current code (serves active) and passes with the fix (serves revoked).
  • gofmt, go vet, and golangci-lint (repo config) clean.

Security Considerations

Fixes a revocation-bypass window: a revoked or deleted API token could keep authenticating for up to the 5-minute cache TTL. No new dependencies, no permission or schema changes. The change improves the security model and adds no new sensitive data handling.

Performance Impact

Negligible. The cache-hit path is unchanged. On a cache miss the change adds one mutex-guarded read of a counter before the DB query and one mutex-guarded compare on write; no new allocations. Matches UserCache.

Documentation Updates

  • (none required)

Deployment Notes

None. No new environment variables, configuration, or migrations.

Checklist

Code Quality

  • My code follows the project's coding standards
  • I have added tests to cover my changes
  • All new and existing tests pass
  • I have run go fmt and go vet (for Go code)
  • I have run pnpm run lint (no frontend change)

Security

  • I have considered security implications
  • Changes maintain or improve the security model
  • Sensitive information has been properly handled

Compatibility

  • Changes are backward compatible
  • Breaking changes are clearly marked and documented (n/a — none)
  • Dependencies are properly updated (n/a — none added)

Documentation

  • Comments are added for non-obvious code

Additional Notes

This mirrors the existing storeMu/epoch guard in backend/pkg/server/auth/users_cache.go; reviewing the two side by side is the quickest check.