fix(auth): harden OIDC flow, token validation, session, and bearer handling

- replay entry covers exp + clock-skew window; callback GET-only (RFC 6749)
- callback rate-limit returns 429 + Retry-After; panic-recovery sets no-store
- cross-chunk integrity for chunked tokens; bounded chunked-token read scan
- HTTPError classification so 5xx/429 token-endpoint responses retry
- redaction of token/code/state in logs; per-source external auth gate
This commit is contained in:
2026-08-14 16:16:26 +01:00
parent 3eb11b07e9
commit 8640451877
19 changed files with 2002 additions and 508 deletions
+21 -11
View File
@@ -118,18 +118,15 @@ func (trm *TokenResilienceManager) ExecuteTokenOperation(ctx context.Context, op
return fn()
}
// Compose resilience mechanisms
// Compose resilience mechanisms. Retry is the INNER layer (around the
// operation) and the circuit breaker is the OUTER layer, so the breaker
// observes one aggregate result per logical operation rather than one
// failure per retry attempt (which would trip it prematurely — e.g.
// maxFailures=3 + 3 retries opening the breaker on a single brief
// outage, failing the very next genuine request). R106.
var finalOperation func() error = fn
// Wrap with circuit breaker if enabled
if trm.config.CircuitBreakerEnabled && trm.circuitBreaker != nil {
originalOp := finalOperation
finalOperation = func() error {
return trm.circuitBreaker.ExecuteWithContext(ctx, originalOp)
}
}
// Wrap with retry if enabled
// Wrap with retry if enabled (innermost layer)
if trm.config.RetryEnabled && trm.retryExecutor != nil {
originalOp := finalOperation
if singleUse {
@@ -143,6 +140,14 @@ func (trm *TokenResilienceManager) ExecuteTokenOperation(ctx context.Context, op
}
}
// Wrap with circuit breaker if enabled (outermost layer)
if trm.config.CircuitBreakerEnabled && trm.circuitBreaker != nil {
originalOp := finalOperation
finalOperation = func() error {
return trm.circuitBreaker.ExecuteWithContext(ctx, originalOp)
}
}
err := finalOperation()
if err != nil && trm.logger != nil {
@@ -174,7 +179,12 @@ func (trm *TokenResilienceManager) ExecuteTokenRefresh(ctx context.Context, t *T
var result *TokenResponse
var err error
err = trm.ExecuteTokenOperation(ctx, "token_refresh", false, func() error {
// Refresh is single-use (like the auth-code exchange): a client-side timeout
// may mean the provider already processed AND rotated the refresh token, so
// re-presenting it on a retry surfaces invalid_grant and the session is
// cleared — escalating a transient timeout into a full logout. Single-use
// retries only on "never sent" errors (connect/dial), never on timeout.
err = trm.ExecuteTokenOperation(ctx, "token_refresh", true, func() error {
// Call exchangeTokens directly to avoid recursion back to getNewTokenWithRefreshToken
// which would call ExecuteTokenRefresh again, causing infinite loop (issue #67)
result, err = t.exchangeTokens(ctx, "refresh_token", refreshToken, "", "")