aws-sso: renew just after the browser sign-in ends, not before; auto-continue

AWS: CLI sessions refresh only while the user interactive session (browser
sign-in) is valid; a login inside it is silent but inherits its end (green
box 'can be used until your session expires', 2026-10-11). Renewing early
gains nothing, so renew 2 min after the estimated end, forcing a fresh IdP
sign-in while the last 1-hour token still works. Silent renewals keep the
anchor. On an expired-session request failure, renew then send 'continue'
(once per 10 min). New: /aws-sso continue on|off, signed-in HH:MM.
This commit is contained in:
pi@mbp-m1-2020
2026-10-11 17:40:25 +02:00
parent efdaba606e
commit 32a397edb4
3 changed files with 107 additions and 47 deletions
+12 -9
View File
@@ -95,12 +95,12 @@ const H = 3600e3;
const T0 = Date.UTC(2026, 9, 11, 14, 19, 0);
const tok = (fp, accessExp) => ({ refreshFp: fp, accessExpiresAt: accessExp });
test("assess: ok → due (lead window) → expired; unknown start stays ok", () => {
test("assess: ok until the end, due just AFTER it (renew → fresh sign-in), expired once the 1-h token is gone too", () => {
const st = { anchor: { refreshFp: "aaaa1111", since: T0 } };
assert.equal(assess(st, tok("aaaa1111", T0 + H), T0 + 1 * H).status, "ok");
assert.equal(assess(st, tok("aaaa1111", T0 + 8 * H), T0 + 7.5 * H).status, "ok");
assert.equal(assess(st, tok("aaaa1111", T0 + 8 * H), T0 + 8 * H - DEFAULTS.leadMinutes * 60e3).status, "due");
assert.equal(assess(st, tok("aaaa1111", T0 + 8.5 * H), T0 + 8.2 * H).status, "due"); // past end but access token still valid
assert.equal(assess(st, tok("aaaa1111", T0 + 8.5 * H), T0 + 7.9 * H).status, "ok"); // early renewal gains nothing: do not renew before the end
assert.equal(assess(st, tok("aaaa1111", T0 + 8.5 * H), T0 + 8 * H + 60e3).status, "ok"); // inside the grace minutes
assert.equal(assess(st, tok("aaaa1111", T0 + 8.5 * H), T0 + 8 * H + DEFAULTS.renewAfterMinutes * 60e3).status, "due"); // 1-h token still valid: renew without an outage
assert.equal(assess(st, tok("aaaa1111", T0 + 8 * H), T0 + 8.2 * H).status, "expired");
assert.equal(assess(st, null, T0).status, "none");
assert.equal(assess({}, tok("aaaa1111", T0 + H), T0 + 30 * H).status, "ok"); // no anchor: never invent an end
@@ -110,14 +110,17 @@ test("assess: ok → due (lead window) → expired; unknown start stays ok", ()
test("assess honours a configured session length", () => {
const st = { sessionHours: 72, anchor: { refreshFp: "aaaa1111", since: T0 } };
assert.equal(assess(st, tok("aaaa1111", T0 + 9 * H), T0 + 8.2 * H).status, "ok");
assert.equal(assess(st, tok("aaaa1111", T0 + 72 * H), T0 + 71.8 * H).status, "due");
assert.equal(assess(st, tok("aaaa1111", T0 + 72.5 * H), T0 + 72.1 * H).status, "due");
});
test("reanchor: only when the refresh token changes (hourly access refresh keeps it)", () => {
test("reanchor: a new token BEFORE the end inherits it (silent renewal); AFTER the end or an expiry starts fresh", () => {
const st = { anchor: { refreshFp: "aaaa1111", since: T0 } };
assert.equal(reanchor(st, "aaaa1111", T0 + 3 * H), null);
assert.deepEqual(reanchor(st, "bbbb2222", T0 + 3 * H), { refreshFp: "bbbb2222", since: T0 + 3 * H });
assert.deepEqual(reanchor({}, "aaaa1111", T0), { refreshFp: "aaaa1111", since: T0 });
assert.equal(reanchor(st, "aaaa1111", T0 + 3 * H, T0 + 3 * H), null); // hourly access refresh: same refresh token
assert.deepEqual(reanchor(st, "bbbb2222", T0 + 1 * H, T0 + 1 * H), { refreshFp: "bbbb2222", since: T0 }); // measured 2026-10-11: green box, same session
assert.deepEqual(reanchor(st, "bbbb2222", T0 + 8.05 * H, T0 + 8.05 * H), { refreshFp: "bbbb2222", since: T0 + 8.05 * H });
assert.deepEqual(reanchor({ ...st, sessionEnded: true }, "bbbb2222", T0 + 2 * H, T0 + 2 * H), { refreshFp: "bbbb2222", since: T0 + 2 * H });
assert.deepEqual(reanchor({ ...st, sessionHours: 1 }, "bbbb2222", T0 + 2 * H, T0 + 2 * H), { refreshFp: "bbbb2222", since: T0 + 2 * H });
assert.deepEqual(reanchor({}, "aaaa1111", T0, T0), { refreshFp: "aaaa1111", since: T0 });
});
test("fmtLeft", () => {