Audit review (rounds 1-31): fix confirmed issues, remove dead code, harden tests

Security/correctness fixes:
- fail-open token validation preserved with lenient-audience; access token that
  fails verification and has no ID token to corroborate now fails closed
- token_manager blacklist-expiry: fix impossible tautology (x>24h && x<24h)
- constant-time OAuth state compare; clear state/nonce/code_verifier on
  callback token-exchange failure to prevent replay of half-failed flows
- StopAllTasks gated on last-instance close
- background-task panic recovery; remove task-registry counter leak
- circuit breaker half-open: release probe slot; CB + per-session outcome
  recorded once per refresh operation, not once per waiter
- Redis connection pool: handle nil receive when pool closed concurrently
- FileStore: atomic temp-write + rename
- errors_recovery health-check data race -> atomic

Test-isolation / hardening:
- reset helpers for Once-pinned global cache manager and transport pool
- coverage: utils 70->93% (redirect URL security contract); fuzz targets for
  bearer parsers; concurrent ServeHTTP race tests for the shared request path

Dead-code removal (verified orphaned, build+suite green): internal/providers
(30 files, 0 importers), internal/compat, internal/features, input_validation.

Verified: govulncheck, go mod tidy, yaegi-validate, cross-compile (darwin+
linux), golangci-lint (new-from-rev clean), 3x full race+shuffle green.
This commit is contained in:
2026-08-12 23:08:03 +01:00
parent f728f61777
commit 2ff14556d0
66 changed files with 1064 additions and 10046 deletions
+20 -6
View File
@@ -244,14 +244,15 @@ func (rc *RefreshCoordinator) CoordinateRefresh(
operation.mutex.RUnlock()
if result != nil {
// Record metrics based on result
// Per-request counters only. The circuit breaker and per-session
// success/failure trackers must be recorded exactly once per
// refresh OPERATION (in executeRefreshAsync), not once per waiter:
// N requests coalescing onto a single refresh would otherwise
// multiply one upstream result into N failure records and
// over-trigger the circuit breaker (MaxFailures=3).
if result.err != nil {
rc.circuitBreaker.RecordFailure()
rc.recordRefreshFailure(sessionID)
atomic.AddInt64(&rc.metrics.failedRefreshes, 1)
} else {
rc.circuitBreaker.RecordSuccess()
rc.recordRefreshSuccess(sessionID)
atomic.AddInt64(&rc.metrics.successfulRefreshes, 1)
}
return result.tokenResponse, result.err
@@ -371,7 +372,7 @@ func (rc *RefreshCoordinator) failCandidate(tokenHash string, op *refreshOperati
// executeRefreshAsync performs the actual refresh operation asynchronously
func (rc *RefreshCoordinator) executeRefreshAsync(
operation *refreshOperation,
_ string, // sessionID - reserved for future metrics/logging
sessionID string,
tokenHash string,
refreshFunc func() (*TokenResponse, error),
) {
@@ -426,6 +427,19 @@ func (rc *RefreshCoordinator) executeRefreshAsync(
}
operation.mutex.Unlock()
}
// Record the circuit breaker and per-session outcome exactly once per
// refresh operation. This function runs once per operation regardless of
// how many requests coalesced onto it (see CoordinateRefresh), so a
// single upstream refresh yields a single CB/session record rather than
// N (one per waiter), which would over-trigger the circuit breaker.
if err := operation.result.err; err != nil {
rc.circuitBreaker.RecordFailure()
rc.recordRefreshFailure(sessionID)
} else {
rc.circuitBreaker.RecordSuccess()
rc.recordRefreshSuccess(sessionID)
}
}
// scheduleDelayedCleanup schedules a cleanup using a timer instead of spawning