fix(sdk): scale the token refresh lead to the token lifetime - #554
Conversation
The refresh window was a fixed 60 minutes. Staging now issues one-hour access tokens, so a token satisfied that window the moment it was minted and the CLI refreshed on every command. The lead is now half the token's own lifetime, read from the exp and iat claims, with the previous 60 minutes as a ceiling. A one-hour token keeps its first half hour before the first renewal. A 24-hour token still gets a 60-minute lead, so nothing changes for the token lifetimes that were in use before. A token without an iat claim gives no lifetime to measure, so it falls back to five minutes: the same buffer the token stores already use, and a safer guess than assuming the token is long lived. TokenManager had a second copy of the same 60-minute constant. It now defers to TokenSet, so the SDK and the CLI renew on one schedule. Adds the first tests for this module, which had none.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. WalkthroughToken refresh scheduling now derives its refresh window from JWT lifetime data. ChangesToken refresh scheduling
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to The change scales token refresh timing to token lifetime while preserving the existing behavior for long-lived tokens; no actionable merge-blocking risk remains. Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
WalkthroughThis PR replaces the SDK's fixed 60-minute pre-emptive token refresh window with a value derived from each token's own lifetime ( Changes
Sequence Diagram(s)This change alters refresh decision timing but does not wire a new multi-component flow, so a sequence diagram would add nothing. Estimated review effort: 2/5 (small, self-contained timing fix with clear tests and well-documented rationale). Instant overview - a deep technical review follows as a separate comment. |
PR #554: fix(sdk): scale the token refresh lead to the token lifetimeSummaryThe PR replaces the hardcoded 60-minute pre-emptive token refresh lead with one derived from the token's own claims: ArchitectureNo structural impact. The change is local to the SDK auth module ( Issues FoundCRITICAL Issues (Must Fix Before Merge)None found. HIGH Severity Issues (Advised to Fix Before Merge)None found. MEDIUM Severity Issues (Optional to Fix Before Merge)None found. LOW Severity Issues (Minor Improvements)
Security ReviewSurface swept: trust boundaries, input validation, secret handling, injection, dependencies, DoS.
No security findings. Suggestions for Improvements
Positive Observations
Commit standards: the PR title Recommendation and Next StepsAPPROVE — the fix is correct, backward-compatible for all token lifetimes currently in production, well-tested against both failure directions, and carries no security or stability concerns; the single LOW item (a rare second-boundary test flake) is a quick follow-up and does not block merge. |
The problem
The pre-emptive refresh window was a fixed 60 minutes:
That works while access tokens live a lot longer than an hour. Staging's Auth0 API token lifetime has just moved from 86,400s to 3,600s, and at that point the rule degenerates: a token minted one second ago already "expires within 60 minutes", so
needs_refresh()is true for a token's entire life.basilica-cli/src/client.rscalls this on every command, so the CLI hits Auth0's token endpoint on every single invocation —ps,ssh,exec, all of them.Observed on staging after the lifetime change: a token issued at 07:47:19 had already been silently replaced by 07:48:21, about a minute later, with no user action in between.
The same fixed hour appears a second time as
TokenManager::REFRESH_THRESHOLDinsimple_manager.rs, so the SDK and the CLI each carried their own copy of the value.The fix
Derive the window from the token instead of hardcoding it. The lifetime is available from the token's own claims (
exp - iat):Half the lifetime, capped at the previous hour.
iatThe cap is what makes this safe to land: a 24-hour token still gets exactly the 60-minute lead it had before, so this is not a behaviour change for any lifetime currently in use in production. It only fixes the case the fixed value got wrong.
The fallback matters too. With no
iatthere is no lifetime to measure, and the tempting default is the old 3,600s — which would quietly reintroduce the bug for any short-lived token lacking the claim. Five minutes is the same buffertoken_store.rsalready uses, and it fails toward "refresh a bit late" rather than "refresh constantly".TokenManager::should_refreshnow defers toTokenSetand its duplicate constant is gone, so both paths renew on one schedule.Why this matters beyond the noise
It also unblocks tightening the lifetime further. A 900s access token is currently not viable: under the fixed hour it would refresh on every command forever. With the lead derived, 900s yields a 450s lead and behaves correctly. That decision is out of scope here, but this is the change that makes it possible.
Tests
This module had no tests at all, so
is_expired,expires_withinand the JWT decoding were entirely unguarded. Seven added, in pairs — a refresh threshold has two failure directions, and pinning only one of them lets "never refresh" pass as a fix:a_freshly_minted_short_token_does_not_need_refresh— the actual bug; fails against the previous codea_short_token_past_halfway_needs_refresh— its counterweight, so the fix cannot be "never renew"lead_is_half_the_lifetime_for_a_short_token— pins the arithmetic directlylong_lived_tokens_keep_the_one_hour_lead+a_long_token_inside_the_last_hour_needs_refresh— enforce the backward-compatibility claim above, which is otherwise just an assertion in a PR descriptionwithout_iat_the_lead_falls_back_to_the_small_bufferan_expired_token_needs_refresh— boundary where unsigned arithmetic on a negative remaining time would go wrongAll green, plus the full suites: 212 SDK, 89 CLI. Clippy and fmt clean.
Scope
Deliberately only the timing. Two related issues found in the same investigation are not addressed here:
simple_manager::get_access_tokenpropagates a failed refresh with?instead of falling back to the still-valid cached token, unlikeclient.rswhich warns and continues.Internal server error: Failed to get access token: Network error: Token refresh failed— three inaccuracies in one line, and no hint to runbasilica login.Both are error-handling rather than timing, and the second is already improved by the
AuthenticationErrorKindwork in the all-sessions-logout PR.Merge order
This should go in before the all-sessions-logout PR. That branch also touches the SDK auth module, and landing the smaller, self-contained change first keeps the two diffs from tangling.
Summary by CodeRabbit