aws-sso: detect the real session end by a refresh attempt, renew before requests fail
Measured: a session lasted <= 6 h (2026-10-11), not the 8 h assumed, so a clock cannot predict the end. token-providers 3.1127.0 refreshes only inside a 5-min window and, on refusal, silently returns the still-valid token. The keeper now makes that attempt itself (pi's own SDK, same cache) when the window opens, and if expiresAt did not advance, logs in while the old token still works. Clock-based renewal, signed-in and hours commands removed.
This commit is contained in:
+36
-15
@@ -5,7 +5,7 @@ import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
fingerprint, cacheKeyFor, parseIni, resolveSsoSession, parseDeviceOutput,
|
||||
isSsoExpiredError, assess, reanchor, fmtLeft, DEFAULTS,
|
||||
isSsoExpiredError, reanchor, fmtLeft, probeDue, probeWith, REFRESH_WINDOW_MS,
|
||||
} from "../extensions/aws-sso.ts";
|
||||
|
||||
const START = "https://d-1234567890.awsapps.com/start";
|
||||
@@ -95,22 +95,43 @@ const H = 3600e3;
|
||||
const T0 = Date.UTC(2026, 9, 11, 14, 19, 0);
|
||||
const tok = (fp, accessExp) => ({ refreshFp: fp, accessExpiresAt: accessExp });
|
||||
|
||||
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.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
|
||||
assert.equal(assess(st, tok("bbbb2222", T0 + H), T0 + 30 * H).endsAt, null); // anchor belongs to another token
|
||||
test("probeDue: only inside the SDK's 5-min window; 60 s apart while valid, 10 min apart once expired", () => {
|
||||
const exp = T0 + H;
|
||||
assert.equal(REFRESH_WINDOW_MS, 5 * 60e3); // token-providers 3.1127.0 EXPIRE_WINDOW_MS
|
||||
assert.equal(probeDue(exp, exp - 6 * 60e3, 0), false); // outside: the SDK would not even try
|
||||
assert.equal(probeDue(exp, exp - 5 * 60e3, 0), true);
|
||||
assert.equal(probeDue(exp, exp - 4 * 60e3, exp - 4.5 * 60e3), false); // 30 s after the last attempt
|
||||
assert.equal(probeDue(exp, exp - 3 * 60e3, exp - 4.5 * 60e3), true);
|
||||
assert.equal(probeDue(exp, exp + 5 * 60e3, exp + 1 * 60e3), false); // expired: back off
|
||||
assert.equal(probeDue(exp, exp + 12 * 60e3, exp + 1 * 60e3), true);
|
||||
});
|
||||
|
||||
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.5 * H), T0 + 72.1 * H).status, "due");
|
||||
// probeWith: a fake SDK provider and a fake cache, mirroring token-providers' real behaviour.
|
||||
const cacheSim = (exp) => ({ exp, read() { return this.exp; } });
|
||||
test("probeWith: refreshed when the cache advances; refused when the SDK silently keeps the old token", async () => {
|
||||
const now = () => T0 + H - 3 * 60e3;
|
||||
const ok = cacheSim(T0 + H);
|
||||
let r = await probeWith(() => () => async () => { ok.exp = T0 + 2 * H; return { token: "x" }; }, () => ok.read(), "Base", now);
|
||||
assert.equal(r.result, "refreshed");
|
||||
const ended = cacheSim(T0 + H); // session over: refresh fails inside the window, SDK returns the old token, no throw
|
||||
r = await probeWith(() => () => async () => ({ token: "old" }), () => ended.read(), "Base", now);
|
||||
assert.equal(r.result, "refused");
|
||||
r = await probeWith(() => () => async () => { throw new Error("Token is expired. To refresh this SSO session run 'aws sso login'"); }, () => ended.read(), "Base", () => T0 + 2 * H);
|
||||
assert.equal(r.result, "refused");
|
||||
assert.match(r.error, /expired/);
|
||||
});
|
||||
|
||||
test("probeWith: never calls the SDK outside the window; a missing SDK is 'unavailable', not 'session ended'", async () => {
|
||||
let called = 0;
|
||||
const c = cacheSim(T0 + H);
|
||||
let r = await probeWith(() => () => async () => { called++; }, () => c.read(), "Base", () => T0);
|
||||
assert.equal(r.result, "fresh");
|
||||
assert.equal(called, 0);
|
||||
r = await probeWith(() => { throw new Error("Cannot find module '@aws-sdk/token-providers'"); }, () => c.read(), "Base", () => T0 + H - 60e3);
|
||||
assert.equal(r.result, "unavailable");
|
||||
let seen;
|
||||
await probeWith(() => (init) => { seen = init; return async () => {}; }, () => c.read(), "GFTLIoT-Base", () => T0 + H - 60e3);
|
||||
assert.deepEqual(seen, { profile: "GFTLIoT-Base", ignoreCache: true }); // the SSO profile at the end of the chain
|
||||
});
|
||||
|
||||
test("reanchor: a new token BEFORE the end inherits it (silent renewal); AFTER the end or an expiry starts fresh", () => {
|
||||
|
||||
Reference in New Issue
Block a user