on main help sign in
fix(oauth): stop fabricating token lifetime when expires_in is missing

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.

commitc83418d10ba6c166419d1ff781380c3a0b0b0ca1 authorKieran Klukas <kieran@dunkirk.sh> dateFri 29 May 2026, 14:36 (5 months ago) parent9019775
1 file changed+21−10
internal/oauth/token.go+21−10
internal/oauth/token.go+21−10
@@ -19,24 +19,35 @@ type Token struct {
1919}
2020
2121// SetExpiresAt calculates and sets the ExpiresAt field based on the
22−// current time and ExpiresIn. If ExpiresIn is zero or negative, it
23−// defaults to 3600 seconds and logs a warning.
22+// current time and ExpiresIn. If ExpiresIn is zero or negative and
23+// ExpiresAt is already set (e.g. from the provider response), it is
24+// left unchanged. If neither is usable, ExpiresAt is set to zero so
25+// IsExpired treats the token as immediately expired, forcing a refresh
26+// rather than guessing a lifetime.
2427func (t *Token) SetExpiresAt() {
25− if t.ExpiresIn <= 0 {
26− slog.Warn("OAuth token has invalid expires_in, defaulting to 3600s", "expires_in", t.ExpiresIn)
27− t.ExpiresIn = 3600
28+ if t.ExpiresIn > 0 {
29+ t.ExpiresAt = time.Now().Add(time.Duration(t.ExpiresIn) * time.Second).Unix()
30+ return
2831 }
29− t.ExpiresAt = time.Now().Add(time.Duration(t.ExpiresIn) * time.Second).Unix()
32+ // ExpiresIn is missing or invalid. If ExpiresAt was already
33+ // populated by the provider (some return exp directly), trust it.
34+ if t.ExpiresAt > 0 {
35+ slog.Warn("OAuth token has invalid expires_in but valid expires_at, using expires_at",
36+ "expires_in", t.ExpiresIn, "expires_at", t.ExpiresAt)
37+ return
38+ }
39+ // Neither field is usable. Mark as expired so the caller is forced
40+ // to refresh rather than operating with a fabricated lifetime.
41+ slog.Warn("OAuth token has no valid expiry information, marking as expired",
42+ "expires_in", t.ExpiresIn, "expires_at", t.ExpiresAt)
43+ t.ExpiresAt = 0
3044}
3145
3246// IsExpired checks if the token is expired or about to expire. It
3347// uses a buffer of max(expires_in/10, minRefreshBuffer) seconds to
3448// trigger proactive refresh before the token actually expires.
3549func (t *Token) IsExpired() bool {
36− buffer := int64(t.ExpiresIn) / 10
37− if buffer < minRefreshBuffer {
38− buffer = minRefreshBuffer
39− }
50+ buffer := max(int64(t.ExpiresIn)/10, minRefreshBuffer)
4051 return time.Now().Unix() >= (t.ExpiresAt - buffer)
4152}
4253