149 lines
6.9 KiB
TypeScript
149 lines
6.9 KiB
TypeScript
|
|
import { createHmac } from "node:crypto";
|
||
|
|
import { describe, expect, it } from "vitest";
|
||
|
|
|
||
|
|
import {
|
||
|
|
addinTicketSecret,
|
||
|
|
parseTicketTokenBody,
|
||
|
|
ticketToDocverClaims,
|
||
|
|
verifyAddinTicket,
|
||
|
|
} 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);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
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;
|
||
|
|
});
|
||
|
|
});
|