// Run the shipped session code against a hub that behaves like the real one. // // The defect this guards was not subtle once seen: the hub rotates refresh // tokens — it revokes the one presented and returns a replacement, and a // revoked token presented again revokes the entire family — and the client kept // only the access token out of that response. So the refresh token was spent on // first use and the second attempt locked the account out of renewal // altogether, leaving signing out and back in as the only way to carry on. // // The fake hub below enforces exactly that rule, which is the point: a stub // that happily accepts the same refresh token twice would have passed against // the broken client. import { readFileSync } from 'fs'; const app = readFileSync(process.argv[2], 'utf8'); const cfg = JSON.parse(process.argv[3] || '{}'); const { scenario = 'expired-access' } = cfg; // ── The pieces of app.js under test, lifted as text ────────────────────────── const between = (from, to) => { const i = app.indexOf(from); if (i < 0) throw new Error(`not found: ${from}`); const j = app.indexOf(to, i); if (j < 0) throw new Error(`not found: ${to}`); return app.slice(i, j); }; const sessionBlock = between('let _auth = loadAuth();', '\n// ── Hub API'); const hubFetchFn = between('async function hubFetch(', '\nexport {'); // ── The world ──────────────────────────────────────────────────────────────── const store = new Map(); const localStorage = { getItem: (k) => (store.has(k) ? store.get(k) : null), setItem: (k, v) => store.set(k, v), removeItem: (k) => store.delete(k), }; const AUTH_KEY = 'mb_auth'; const HUB = ''; const TOKEN_RENEW_MARGIN_S = Number(app.match(/const TOKEN_RENEW_MARGIN_S = (\d+)/)[1]); const jwt = (secondsLeft) => { const payload = Buffer.from(JSON.stringify({ exp: Math.floor(Date.now() / 1000) + secondsLeft, })).toString('base64url'); return `head.${payload}.sig`; }; // The hub. One live refresh token at a time; presenting a revoked one kills the // family, as the real endpoint does. let live = 'RT-1'; let issued = 1; let familyRevoked = false; const seen = []; let refreshCalls = 0; const fakeFetch = async (url, opts = {}) => { if (url.endsWith('/v1/users/token/refresh')) { refreshCalls++; const presented = JSON.parse(opts.body).refresh_token; seen.push(presented); if (familyRevoked || presented !== live) { familyRevoked = true; return { ok: false, status: 401, json: async () => ({ detail: 'Token reuse detected — family revoked' }) }; } live = `RT-${++issued}`; return { ok: true, status: 200, json: async () => ({ access_token: jwt(3600), refresh_token: live, token_type: 'bearer', expires_in: 3600 }), }; } // Any other endpoint: 401 unless the bearer is a token that has life left. const auth = (opts.headers || {})['Authorization'] || ''; const token = auth.replace('Bearer ', ''); let alive = false; try { alive = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString()).exp > Math.floor(Date.now() / 1000); } catch { alive = false; } calls.push({ url, token, alive }); if (!alive) { return { ok: false, status: 401, json: async () => ({ detail: 'Token expired or invalid' }) }; } return { ok: true, status: 200, json: async () => ({ ok: true }) }; }; const calls = []; let _bundleKey = null; const _clearKeyDB = () => {}; function loadAuth() { try { return JSON.parse(localStorage.getItem(AUTH_KEY)); } catch { return null; } } function saveAuth(auth) { if (auth) localStorage.setItem(AUTH_KEY, JSON.stringify(auth)); else { localStorage.removeItem(AUTH_KEY); _bundleKey = null; _clearKeyDB(); } } // The session starts with an access token that expired an hour ago and a // refresh token that is still perfectly good — a tab reopened the next morning. const startingLife = scenario === 'fresh-access' ? 3600 : scenario === 'nearly-expired' ? 60 : -60; localStorage.setItem(AUTH_KEY, JSON.stringify({ username: 'someone', userId: 'u1', role: 'user', token: jwt(startingLife), refreshToken: 'RT-1', })); const ctx = { localStorage, AUTH_KEY, HUB, TOKEN_RENEW_MARGIN_S, loadAuth, saveAuth, fetch: fakeFetch, atob: (s) => Buffer.from(s, 'base64').toString('binary'), console, // Hub calls go through the platform adapter since the interface started // shipping in a package: in a browser it is `fetch`, in the application it is // the main process, because an `app://` origin is refused by CORS. The // harness models the browser side, which is the one whose renewal logic this // exercises. platform: { apiFetch: (...args) => fakeFetch(...args) }, }; const fns = new Function(...Object.keys(ctx), sessionBlock + '\n' + hubFetchFn + '\n return {hubFetch, refreshAccessToken, ensureFreshToken, tokenLifeLeft,' + ' getAuth: () => _auth};')(...Object.values(ctx)); // ── The run ────────────────────────────────────────────────────────────────── const out = { scenario }; const stored = () => JSON.parse(localStorage.getItem(AUTH_KEY) || 'null'); if (scenario === 'concurrent') { // Two requests hit a dead token at the same instant. If each renews on its // own, the second presents a token the first has already spent, and the hub // tears down the family. const before = stored().token; // Either may reject once the family is gone; the run still has to report. await Promise.allSettled([ fns.hubFetch('/v1/groups/mine', { token: before }), fns.hubFetch('/v1/users/me', { token: before }), ]); } else if (scenario === 'repeat') { // Renew several times over. Each response carries a new refresh token, and // keeping the old one would be caught on the very next attempt. for (let i = 0; i < 4; i++) { await fns.refreshAccessToken(); // Age the access token again so the next one really has to renew. A // rejected renewal signs the session out, and there is then nothing left // to age — which is the outcome under test, not a reason to stop. const a = stored(); if (!a) break; localStorage.setItem(AUTH_KEY, JSON.stringify({ ...a, token: jwt(-60) })); const live_ = fns.getAuth(); if (live_) live_.token = jwt(-60); } } else if (scenario === 'refresh-rejected') { localStorage.setItem(AUTH_KEY, JSON.stringify({ ...stored(), refreshToken: 'RT-STALE' })); fns.getAuth().refreshToken = 'RT-STALE'; try { await fns.hubFetch('/v1/groups/mine', { token: stored().token }); } catch (e) { out.threw = String(e.message); } } else { await fns.ensureFreshToken(); await fns.hubFetch('/v1/groups/mine', { token: stored() ? stored().token : null }); } out.refreshCalls = refreshCalls; out.refreshTokensPresented = seen; out.familyRevoked = familyRevoked; out.signedOut = stored() === null; out.storedRefreshToken = stored() ? stored().refreshToken : null; out.apiCalls = calls.map(c => ({ url: c.url, accepted: c.alive })); out.everRejected = calls.some(c => !c.alive); out.finalCallAccepted = calls.length ? calls[calls.length - 1].alive : null; console.log(JSON.stringify(out));