fix(oauth): stop fabricating token lifetime when expires_in is missing

Kieran Klukas created

SetExpiresAt no longer silently defaults to 3600s when ExpiresIn is
zero or negative. If ExpiresAt was already set by the provider (some
return exp directly), it is preserved. Otherwise the token is marked
as immediately expired (ExpiresAt=0) so callers are forced to refresh
rather than operating with a guessed lifetime.

Also modernizes IsExpired to use max() for the buffer calculation.

Change summary

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

Detailed changes

internal/oauth/token.go 🔗

@@ -19,24 +19,35 @@ type Token struct {
 }
 
 // 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.
+// current time and ExpiresIn. If ExpiresIn is zero or negative and
+// ExpiresAt is already set (e.g. from the provider response), it is
+// left unchanged. If neither is usable, ExpiresAt is set to zero so
+// IsExpired treats the token as immediately expired, forcing a refresh
+// rather than guessing a lifetime.
 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
+	if t.ExpiresIn > 0 {
+		t.ExpiresAt = time.Now().Add(time.Duration(t.ExpiresIn) * time.Second).Unix()
+		return
 	}
-	t.ExpiresAt = time.Now().Add(time.Duration(t.ExpiresIn) * time.Second).Unix()
+	// ExpiresIn is missing or invalid. If ExpiresAt was already
+	// populated by the provider (some return exp directly), trust it.
+	if t.ExpiresAt > 0 {
+		slog.Warn("OAuth token has invalid expires_in but valid expires_at, using expires_at",
+			"expires_in", t.ExpiresIn, "expires_at", t.ExpiresAt)
+		return
+	}
+	// Neither field is usable. Mark as expired so the caller is forced
+	// to refresh rather than operating with a fabricated lifetime.
+	slog.Warn("OAuth token has no valid expiry information, marking as expired",
+		"expires_in", t.ExpiresIn, "expires_at", t.ExpiresAt)
+	t.ExpiresAt = 0
 }
 
 // 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 {
-	buffer := int64(t.ExpiresIn) / 10
-	if buffer < minRefreshBuffer {
-		buffer = minRefreshBuffer
-	}
+	buffer := max(int64(t.ExpiresIn)/10, minRefreshBuffer)
 	return time.Now().Unix() >= (t.ExpiresAt - buffer)
 }