fix: Throttle audit logging for expired API token use

Every request presenting an expired token wrote a token.expired_use
audit row, and RateLimitTokens only applies once a session exists, so a
rejected token was never rate-limited. A looping job with one expired
token could write an unbounded number of audit rows, drowning the real
audit trail.

services.ShouldLogExpiredTokenUse now dedupes to at most one
token.expired_use record per token per minute, mirroring the throttle
TouchAPIToken already uses for last-used. It lives in services rather
than auth because the storage concern belongs beside the token's other
storage-backed state. The first use per window is still recorded, which
is what makes a forgotten job visible.
This commit is contained in:
2026-08-13 08:31:12 +00:00
parent 965419b2b8
commit 225b53bfa7
+7 -3
View File
@@ -135,9 +135,13 @@ func sessionFromToken(c *gin.Context) (*Session, bool) {
tok, err := services.ResolveAPIToken(raw)
if errors.Is(err, services.ErrTokenExpired) {
// Recorded rather than only refused: an expired token still being
// presented is how a forgotten CI job becomes visible.
services.LogEvent(tok.InstanceID, "token.expired_use", tok.Name, "", "",
fmt.Sprintf("expired token '%s' was used", tok.Name))
// presented is how a forgotten CI job becomes visible. Throttled to
// once per token per minute, or a looping job writes an unbounded
// stream of audit rows instead of one.
if services.ShouldLogExpiredTokenUse(tok.TokenID) {
services.LogEvent(tok.InstanceID, "token.expired_use", tok.Name, "", "",
fmt.Sprintf("expired token '%s' was used", tok.Name))
}
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "token expired", "code": "token_expired"})
return nil, false
}