import { describe, it, expect } from "vitest"; import { createHmac } from "node:crypto"; import { verifyHandshakeToken, EXPIRY_GRACE_MS, EXPECTED_ISSUER } from "../src/auth.js"; const SECRET = "unit-test-jwt-secret-0123456789abcdef"; function b64url(buf: Buffer | string): string { return Buffer.from(buf, "utf8").toString("base64url"); } /** Hand-build a HS256 JWT with arbitrary claims (the API mints `workspaces`, * which `@factory/auth`'s `signAccessToken` does not — so the test signs its * own to exercise every handshake path). */ function jwt(claims: Record, secret: string = SECRET): string { const header = b64url(JSON.stringify({ alg: "HS256", typ: "JWT" })); const body = b64url(JSON.stringify(claims)); const sig = createHmac("sha256", secret) .update(`${header}.${body}`) .digest() .toString("base64url"); return `${header}.${body}.${sig}`; } const NOW = 1_700_000_000_000; // fixed ms clock for determinism const nowSec = Math.floor(NOW / 1000); describe("verifyHandshakeToken (WS handshake auth, R1)", () => { it("verifies a valid token with a non-empty workspaces claim", () => { const token = jwt({ sub: "usr_1", exp: nowSec + 3600, jti: "jti_1", workspaces: ["wks_1", "wks_2"] }); const r = verifyHandshakeToken(token, SECRET, NOW); expect(r).toEqual({ ok: true, session: { userId: "usr_1", workspaces: ["wks_1", "wks_2"], jti: "jti_1", exp: nowSec + 3600 }, }); }); it("returns missing_token for null/empty token", () => { expect(verifyHandshakeToken(null, SECRET, NOW)).toEqual({ ok: false, reason: "missing_token" }); expect(verifyHandshakeToken("", SECRET, NOW)).toEqual({ ok: false, reason: "missing_token" }); expect(verifyHandshakeToken(undefined, SECRET, NOW)).toEqual({ ok: false, reason: "missing_token" }); }); it("returns invalid_token for a garbage (non-JWT) string", () => { expect(verifyHandshakeToken("garbage-not-a-jwt", SECRET, NOW)).toEqual({ ok: false, reason: "invalid_token" }); }); it("returns invalid_token when the signature does not match the secret", () => { const token = jwt({ sub: "usr_1", exp: nowSec + 3600, workspaces: ["wks_1"] }, "a-different-secret-0123456789"); expect(verifyHandshakeToken(token, SECRET, NOW)).toEqual({ ok: false, reason: "invalid_token" }); }); it("returns wrong_issuer when `iss` is present but not the expected issuer", () => { const token = jwt({ sub: "usr_1", exp: nowSec + 3600, iss: "evil", workspaces: ["wks_1"] }); expect(verifyHandshakeToken(token, SECRET, NOW)).toEqual({ ok: false, reason: "wrong_issuer" }); }); it("accepts a token whose `iss` is the expected issuer", () => { const token = jwt({ sub: "usr_1", exp: nowSec + 3600, iss: EXPECTED_ISSUER, workspaces: ["wks_1"] }); expect(verifyHandshakeToken(token, SECRET, NOW)).toMatchObject({ ok: true, session: { userId: "usr_1" } }); }); it("accepts a token with no `iss` claim at all (iss is optional — note 13)", () => { const token = jwt({ sub: "usr_1", exp: nowSec + 3600, workspaces: ["wks_1"] }); expect(verifyHandshakeToken(token, SECRET, NOW)).toMatchObject({ ok: true }); }); it("returns no_workspaces when the workspaces claim is absent", () => { const token = jwt({ sub: "usr_1", exp: nowSec + 3600 }); expect(verifyHandshakeToken(token, SECRET, NOW)).toEqual({ ok: false, reason: "no_workspaces" }); }); it("returns no_workspaces when the workspaces claim is an empty array", () => { const token = jwt({ sub: "usr_1", exp: nowSec + 3600, workspaces: [] }); expect(verifyHandshakeToken(token, SECRET, NOW)).toEqual({ ok: false, reason: "no_workspaces" }); }); it("filters non-string workspace entries and still succeeds when a string remains", () => { const token = jwt({ sub: "usr_1", exp: nowSec + 3600, workspaces: ["wks_1", 42, null] }); const r = verifyHandshakeToken(token, SECRET, NOW); expect(r).toMatchObject({ ok: true, session: { workspaces: ["wks_1"] } }); }); it("returns no_workspaces when every workspace entry is non-string", () => { const token = jwt({ sub: "usr_1", exp: nowSec + 3600, workspaces: [42, null, { a: 1 }] }); expect(verifyHandshakeToken(token, SECRET, NOW)).toEqual({ ok: false, reason: "no_workspaces" }); }); it("accepts a token expired by less than the 5-min grace (an open tab)", () => { // expired 60 s ago (well inside the 300 s grace) const token = jwt({ sub: "usr_1", exp: nowSec - 60, workspaces: ["wks_1"] }); const r = verifyHandshakeToken(token, SECRET, NOW); expect(r).toMatchObject({ ok: true, session: { userId: "usr_1" } }); }); it("rejects a token expired by more than the grace", () => { const token = jwt({ sub: "usr_1", exp: nowSec - (EXPIRY_GRACE_MS / 1000) - 60, workspaces: ["wks_1"] }); expect(verifyHandshakeToken(token, SECRET, NOW)).toEqual({ ok: false, reason: "invalid_token" }); }); });