fix(oauth): harden IsExpired with minimum buffer and ExpiresIn guard

Kieran Klukas created

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.

Change summary

internal/oauth/token.go | 24 +++++++++++++++++++++---
1 file changed, 21 insertions(+), 3 deletions(-)

Detailed changes

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.