All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 1m17s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 4s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 1s
- Split `verifyAddinTicket` into two functions: `verifyAddinTicketHmac` (ignores expiration) and `verifyAddinTicket` (checks expiration). - Add `hasVersionByUser` function to check if a document has been previously worked on by a user, allowing expired tickets to be accepted if the user has a history with the document. - Update OpenAPI specification to reflect changes in ticket expiration handling. - Introduce new scripts for installing and uninstalling the Word add-in on Windows and macOS, providing a streamlined process for sideloading the add-in. - Enhance tests for ticket verification to cover new functionality regarding expired tickets and user history.
169 lines
8.4 KiB
TypeScript
169 lines
8.4 KiB
TypeScript
import { createHmac } from "node:crypto";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
addinTicketSecret,
|
|
parseTicketTokenBody,
|
|
ticketToDocverClaims,
|
|
verifyAddinTicket,
|
|
verifyAddinTicketHmac,
|
|
} from "@/lib/addin-ticket";
|
|
|
|
// PLAN-ADDIN-AUTH.md §3 — pure ล้วน (verifyAddinTicket รับ secret ทาง parameter)
|
|
// ไม่แตะ @/db/minio — รันได้โดยไม่ต้องตั้ง env อะไร (เหมือน docver-token.test.ts)
|
|
|
|
const SECRET = "super_secret";
|
|
|
|
/** ตัว ticket ตัวอย่าง — key เรียงตามตัวอักษรเหมือนตัว stamp จริง (sortObjectKeys ของ lib-utils) */
|
|
const TICKET_DATA = JSON.stringify({
|
|
exp: Math.floor(Date.now() / 1000) + 300,
|
|
fid: "T12_FD155_3s-long_ZZZ",
|
|
tid: "T12",
|
|
uid: "oom",
|
|
uname: "สรวิชญ์",
|
|
});
|
|
|
|
function sign(message: string, secret: string = SECRET): string {
|
|
return createHmac("sha256", secret).update(message, "utf8").digest("hex");
|
|
}
|
|
|
|
function ok<T>(result: { ok: boolean } & T): asserts result is { ok: true } & T {
|
|
expect(result.ok).toBe(true);
|
|
}
|
|
|
|
describe("verifyAddinTicket", () => {
|
|
it("ticket ถูกต้อง + HMAC ตรง = ผ่าน ได้ ticket แกะแล้ว", () => {
|
|
const result = verifyAddinTicket(TICKET_DATA, sign(TICKET_DATA), SECRET);
|
|
ok(result);
|
|
expect(result.ticket.fid).toBe("T12_FD155_3s-long_ZZZ");
|
|
expect(result.ticket.uid).toBe("oom");
|
|
expect(result.ticket.tid).toBe("T12");
|
|
});
|
|
|
|
it("HMAC ผิด (แก้ ticket 1 ตัวอักษร / secret ผิด / digest เพี้ยน) = ไม่ผ่านทุกกรณี", () => {
|
|
const digest = sign(TICKET_DATA);
|
|
const tampered = `${TICKET_DATA.slice(0, -2)}xx}`;
|
|
expect(verifyAddinTicket(tampered, digest, SECRET).ok).toBe(false);
|
|
expect(verifyAddinTicket(TICKET_DATA, sign(TICKET_DATA, "wrong-secret"), SECRET).ok).toBe(false);
|
|
const flipped = `${digest.slice(0, -1)}${digest.endsWith("0") ? "1" : "0"}`;
|
|
expect(verifyAddinTicket(TICKET_DATA, flipped, SECRET).ok).toBe(false);
|
|
});
|
|
|
|
it("digest รูปไม่ใช่ (ไม่ใช่ hex / สั้น / ว่าง) = ไม่ผ่าน · uppercase hex normalize แล้วผ่านได้", () => {
|
|
expect(verifyAddinTicket(TICKET_DATA, "zz".repeat(32), SECRET).ok).toBe(false);
|
|
expect(verifyAddinTicket(TICKET_DATA, "abcd", SECRET).ok).toBe(false);
|
|
expect(verifyAddinTicket(TICKET_DATA, "", SECRET).ok).toBe(false);
|
|
expect(verifyAddinTicket(TICKET_DATA, sign(TICKET_DATA).toUpperCase(), SECRET).ok).toBe(true);
|
|
});
|
|
|
|
it("ticket หมดอายุ (exp อดีต / เท่ากับ now) = ไม่ผ่าน แม้ HMAC ถูก", () => {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
for (const exp of [now - 1, now]) {
|
|
const expired = JSON.stringify({ exp, fid: "f", uid: "u", uname: "n" });
|
|
expect(verifyAddinTicket(expired, sign(expired), SECRET, now).ok, `exp=${exp}`).toBe(false);
|
|
}
|
|
const alive = JSON.stringify({ exp: now + 1, fid: "f", uid: "u", uname: "n" });
|
|
expect(verifyAddinTicket(alive, sign(alive), SECRET, now).ok).toBe(true);
|
|
});
|
|
|
|
it("HMAC ถูกแต่ ticket ผิดกติกา (field หลุด allowlist / exp ไม่ใช่ integer) = ไม่ผ่าน", () => {
|
|
const extra = JSON.stringify({ exp: Math.floor(Date.now() / 1000) + 60, fid: "f", uid: "u", uname: "n", oops: 1 });
|
|
expect(verifyAddinTicket(extra, sign(extra), SECRET).ok).toBe(false);
|
|
const badExp = JSON.stringify({ exp: "123", fid: "f", uid: "u", uname: "n" });
|
|
expect(verifyAddinTicket(badExp, sign(badExp), SECRET).ok).toBe(false);
|
|
});
|
|
});
|
|
|
|
// verifyAddinTicketHmac = ตัวเดียวกันแต่ไม่ตรวจ exp — route ใช้ตรวจ HMAC ก่อน
|
|
// แล้วตัดสิน "ticket หมดอายุ + เอกสารนี้เคยทำงานโดย uid เดียวกัน" จาก DB เอง
|
|
// (requirement ขั้น 7 — แก้บั๊ก 2026-10-28)
|
|
describe("verifyAddinTicketHmac", () => {
|
|
it("exp อดีต + HMAC ถูก = ผ่าน (ได้ ticket ครบ) — ต่างจาก verifyAddinTicket ที่ไม่ผ่าน", () => {
|
|
const expired = JSON.stringify({ exp: 1, fid: "f", uid: "u", uname: "n" });
|
|
const hmacResult = verifyAddinTicketHmac(expired, sign(expired), SECRET);
|
|
ok(hmacResult);
|
|
expect(hmacResult.ticket.uid).toBe("u");
|
|
expect(verifyAddinTicket(expired, sign(expired), SECRET).ok).toBe(false);
|
|
});
|
|
|
|
it("HMAC ผิด / digest รูปเพี้ยน / ticket ผิดกติกา = ไม่ผ่านเหมือนเดิม (แม้ exp ยังไม่หมด)", () => {
|
|
expect(verifyAddinTicketHmac(TICKET_DATA, sign(TICKET_DATA, "wrong-secret"), SECRET).ok).toBe(false);
|
|
expect(verifyAddinTicketHmac(TICKET_DATA, "zz".repeat(32), SECRET).ok).toBe(false);
|
|
const extra = JSON.stringify({ exp: Math.floor(Date.now() / 1000) + 60, fid: "f", uid: "u", uname: "n", oops: 1 });
|
|
expect(verifyAddinTicketHmac(extra, sign(extra), SECRET).ok).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("ticketToDocverClaims", () => {
|
|
it("แปลง uid→userid, uname→username, tid→template · tid ไม่มี = null", () => {
|
|
expect(
|
|
ticketToDocverClaims({ exp: 1, fid: "F1", tid: "T12", uid: "oom", uname: "สรวิชญ์" }),
|
|
).toEqual({ userid: "oom", username: "สรวิชญ์", fid: "F1", template: "T12" });
|
|
expect(
|
|
ticketToDocverClaims({ exp: 1, fid: "F1", tid: null, uid: "oom", uname: "สรวิชญ์" }),
|
|
).toEqual({ userid: "oom", username: "สรวิชญ์", fid: "F1", template: null });
|
|
});
|
|
});
|
|
|
|
describe("parseTicketTokenBody", () => {
|
|
it("ครบทั้งคู่ = ได้ค่าตรง ๆ (ไม่แตะค่า — HMAC ต้องคำนวณบน string ดิบ)", () => {
|
|
const digest = sign(TICKET_DATA);
|
|
const result = parseTicketTokenBody({ ticket_data: TICKET_DATA, HmacSha256: digest });
|
|
ok(result);
|
|
expect(result.ticketData).toBe(TICKET_DATA);
|
|
expect(result.hmacHex).toBe(digest);
|
|
});
|
|
|
|
it("ไม่ใช่ object / มี field อื่นปน = invalid (→ 400)", () => {
|
|
for (const raw of [[], "text", null, { ticket_data: "x", HmacSha256: "y", extra: 1 }]) {
|
|
const result = parseTicketTokenBody(raw);
|
|
expect(result.ok, JSON.stringify(raw)).toBe(false);
|
|
if (!result.ok) expect(result.kind).toBe("invalid");
|
|
}
|
|
});
|
|
|
|
it("ขาด field (ไม่ส่ง/ว่าง/null) = missing (→ 401 กัน oracle)", () => {
|
|
for (const raw of [{}, { ticket_data: "" }, { HmacSha256: "y" }, { ticket_data: null, HmacSha256: "y" }]) {
|
|
const result = parseTicketTokenBody(raw);
|
|
expect(result.ok, JSON.stringify(raw)).toBe(false);
|
|
if (!result.ok) expect(result.kind).toBe("missing");
|
|
}
|
|
});
|
|
|
|
it("field ไม่ใช่ string / ยาวเกิน cap = invalid", () => {
|
|
for (const raw of [
|
|
{ ticket_data: 123, HmacSha256: "y" },
|
|
{ ticket_data: "x", HmacSha256: "y".repeat(129) },
|
|
{ ticket_data: "x".repeat(2049), HmacSha256: "y" },
|
|
]) {
|
|
const result = parseTicketTokenBody(raw);
|
|
expect(result.ok, JSON.stringify(raw)).toBe(false);
|
|
if (!result.ok) expect(result.kind).toBe("invalid");
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("addinTicketSecret", () => {
|
|
// env save/restore — addinTicketSecret อ่านตอนเรียก
|
|
const ORIG_ADDIN = process.env.ADDIN_TICKET_SECRET;
|
|
const ORIG_SECRET = process.env.SECRET;
|
|
|
|
it("ลำดับ ADDIN_TICKET_SECRET → SECRET → default \"super_secret\" (trim ทุกตัว)", () => {
|
|
delete process.env.ADDIN_TICKET_SECRET;
|
|
delete process.env.SECRET;
|
|
expect(addinTicketSecret()).toBe("super_secret");
|
|
|
|
process.env.SECRET = " tmpl-secret ";
|
|
expect(addinTicketSecret()).toBe("tmpl-secret");
|
|
|
|
process.env.ADDIN_TICKET_SECRET = " addin-secret ";
|
|
expect(addinTicketSecret()).toBe("addin-secret");
|
|
|
|
delete process.env.ADDIN_TICKET_SECRET;
|
|
process.env.SECRET = " "; // ว่างล้วน = ข้ามไปตัวถัดไป
|
|
expect(addinTicketSecret()).toBe("super_secret");
|
|
|
|
process.env.ADDIN_TICKET_SECRET = ORIG_ADDIN;
|
|
process.env.SECRET = ORIG_SECRET;
|
|
});
|
|
});
|