← back to Letsbegin

__tests__/session-token.test.mjs

69 lines

/**
 * session-token.test.mjs — PROOF for the TK-11776 follow-up (Letsbegin local session).
 *
 * The local `letsbegin_session` cookie used to be an UNSIGNED base64(JSON) blob that
 * middleware.ts + /api/auth/session trusted on its `authenticated` flag alone, so
 * `btoa('{"authenticated":true,"loginTime":<now>}')` was a valid session. This proves the
 * new HMAC signer REJECTS that forgery, ACCEPTS a validly-signed session, and fails closed
 * on wrong-secret / tampered / unset-secret. Runnable with plain
 * `node __tests__/session-token.test.mjs` (Node 23.6+/26 imports the .ts lib directly).
 */
import assert from 'node:assert/strict';
import { signSession, verifySession } from '../lib/session-token.ts';

const FIXTURE = 'hmac-test-fixture';
// the exact live forgery: an unsigned base64(JSON) cookie claiming authenticated:true
const forge = (extra = {}) =>
  Buffer.from(JSON.stringify({ authenticated: true, loginTime: Date.now(), ...extra })).toString('base64');

// the vulnerable OLD logic, verbatim — used only to prove this test is a real detector
function oldVerify(token) {
  try {
    const d = JSON.parse(Buffer.from(token, 'base64').toString());
    return d.authenticated === true;
  } catch {
    return false;
  }
}

let pass = 0;
const check = async (name, fn) => { await fn(); pass++; console.log(`  ✓ ${name}`); };

console.log('TK-11776 follow-up — forgeable letsbegin_session is closed');

await check('detector sanity: OLD logic ACCEPTS the forged authenticated cookie', async () => {
  assert.equal(oldVerify(forge()), true);
});
await check('REJECTS the forged unsigned base64(JSON) cookie (the live exploit) -> null', async () => {
  assert.equal(await verifySession(forge(), FIXTURE), null);
});
await check('ACCEPTS a validly-signed session and returns its fields', async () => {
  const token = await signSession({ authenticated: true, username: 'steve', loginTime: Date.now() }, FIXTURE);
  const s = await verifySession(token, FIXTURE);
  assert.ok(s && s.authenticated === true, 'authenticated should be true');
  assert.equal(s.username, 'steve');
  assert.equal(typeof s.loginTime, 'number');
});
await check('REJECTS a valid token under the WRONG secret -> null', async () => {
  const token = await signSession({ authenticated: true, loginTime: Date.now() }, FIXTURE);
  assert.equal(await verifySession(token, 'a-different-secret'), null);
});
await check('REJECTS a tampered payload with a stolen signature -> null', async () => {
  const good = await signSession({ authenticated: false, loginTime: Date.now() }, FIXTURE);
  const sig = good.slice(good.lastIndexOf('.'));
  const tampered = `${Buffer.from(JSON.stringify({ authenticated: true, loginTime: Date.now() })).toString('base64')}${sig}`;
  assert.equal(await verifySession(tampered, FIXTURE), null);
});
await check('FAILS CLOSED when the secret is unset -> verify null, sign throws', async () => {
  const token = await signSession({ authenticated: true, loginTime: Date.now() }, FIXTURE);
  assert.equal(await verifySession(token, undefined), null);
  await assert.rejects(() => signSession({ authenticated: true }, undefined));
});
await check('REJECTS junk / signatureless / empty tokens -> null', async () => {
  for (const t of ['', 'x', 'not.base64.parts', forge(), null, undefined]) {
    assert.equal(await verifySession(t, FIXTURE), null);
  }
});

console.log(`\n${pass} passed`);