From d87a632af1cd528b8ed76adbc3f45041acd2b128 Mon Sep 17 00:00:00 2001 From: Kieran Klukas Date: Thu, 28 May 2026 14:38:44 -0400 Subject: [PATCH] fix(oauth): harden IsExpired with minimum buffer and ExpiresIn guard Add 30-second minimum refresh buffer so short-lived tokens always have a meaningful proactive refresh window. Guard against ExpiresIn=0 in SetExpiresAt by defaulting to 3600s with a warning, preventing tokens from being dead on arrival. --- internal/oauth/token.go | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/internal/oauth/token.go b/internal/oauth/token.go index 381eb3e3110d01db1944fcd98659d87ac7055e2a..1c66374a0f4c10611e9395da676fde83565b480f 100644 --- a/internal/oauth/token.go +++ b/internal/oauth/token.go @@ -1,9 +1,15 @@ package oauth import ( + "log/slog" "time" ) +// minRefreshBuffer is the minimum number of seconds before actual +// expiry at which IsExpired returns true. Prevents very short-lived +// tokens from having a meaningless refresh window. +const minRefreshBuffer = 30 + // Token represents an OAuth2 token. type Token struct { AccessToken string `json:"access_token"` @@ -12,14 +18,26 @@ type Token struct { ExpiresAt int64 `json:"expires_at"` } -// SetExpiresAt calculates and sets the ExpiresAt field based on the current time and ExpiresIn. +// SetExpiresAt calculates and sets the ExpiresAt field based on the +// current time and ExpiresIn. If ExpiresIn is zero or negative, it +// defaults to 3600 seconds and logs a warning. func (t *Token) SetExpiresAt() { + if t.ExpiresIn <= 0 { + slog.Warn("OAuth token has invalid expires_in, defaulting to 3600s", "expires_in", t.ExpiresIn) + t.ExpiresIn = 3600 + } t.ExpiresAt = time.Now().Add(time.Duration(t.ExpiresIn) * time.Second).Unix() } -// IsExpired checks if the token is expired or about to expire (within 10% of its lifetime). +// IsExpired checks if the token is expired or about to expire. It +// uses a buffer of max(expires_in/10, minRefreshBuffer) seconds to +// trigger proactive refresh before the token actually expires. func (t *Token) IsExpired() bool { - return time.Now().Unix() >= (t.ExpiresAt - int64(t.ExpiresIn)/10) + buffer := int64(t.ExpiresIn) / 10 + if buffer < minRefreshBuffer { + buffer = minRefreshBuffer + } + return time.Now().Unix() >= (t.ExpiresAt - buffer) } // SetExpiresIn calculates and sets the ExpiresIn field based on the ExpiresAt field.