forked from bai/curriculum-project-hub
4e7b158ff9
Silo hostname already carries tenancy. Admin SPA routes become /admin/..., legacy /admin/org/:slug/* bookmarks redirect, login lands on /admin.
115 lines
3.4 KiB
TypeScript
115 lines
3.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
signOAuthState,
|
|
signSession,
|
|
verifyOAuthState,
|
|
verifySession,
|
|
} from "../../src/admin/auth/session.js";
|
|
import { sanitizeReturnTo } from "../../src/admin/routes/authRoutes.js";
|
|
|
|
const SECRET = "test-session-secret-not-for-production";
|
|
|
|
describe("session cookie signing", () => {
|
|
it("round-trips a valid session", () => {
|
|
const token = signSession(
|
|
{ userId: "u1", feishuOpenId: "ou_1" },
|
|
SECRET,
|
|
3600,
|
|
1_700_000_000,
|
|
);
|
|
const payload = verifySession(token, SECRET, 1_700_000_100);
|
|
expect(payload).toEqual({
|
|
userId: "u1",
|
|
feishuOpenId: "ou_1",
|
|
iat: 1_700_000_000,
|
|
exp: 1_700_003_600,
|
|
});
|
|
});
|
|
|
|
it("rejects tampered tokens", () => {
|
|
const token = signSession({ userId: "u1", feishuOpenId: "ou_1" }, SECRET);
|
|
const [body] = token.split(".");
|
|
expect(verifySession(`${body}.deadbeef`, SECRET)).toBeNull();
|
|
});
|
|
|
|
it("rejects expired sessions", () => {
|
|
const token = signSession(
|
|
{ userId: "u1", feishuOpenId: "ou_1" },
|
|
SECRET,
|
|
10,
|
|
1_700_000_000,
|
|
);
|
|
expect(verifySession(token, SECRET, 1_700_000_011)).toBeNull();
|
|
});
|
|
|
|
it("rejects wrong secret", () => {
|
|
const token = signSession({ userId: "u1", feishuOpenId: "ou_1" }, SECRET);
|
|
expect(verifySession(token, "other-secret")).toBeNull();
|
|
});
|
|
|
|
it("round-trips a connection-scoped identity", () => {
|
|
const token = signSession({
|
|
userId: "u-scoped",
|
|
feishuIdentityId: "identity-1",
|
|
feishuConnectionId: "connection-1",
|
|
feishuOrganizationId: "organization-1",
|
|
}, SECRET, 3600, 1_700_000_000);
|
|
expect(verifySession(token, SECRET, 1_700_000_100)).toEqual({
|
|
userId: "u-scoped",
|
|
feishuIdentityId: "identity-1",
|
|
feishuConnectionId: "connection-1",
|
|
feishuOrganizationId: "organization-1",
|
|
iat: 1_700_000_000,
|
|
exp: 1_700_003_600,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("oauth state signing", () => {
|
|
it("round-trips state with returnTo", () => {
|
|
const token = signOAuthState(
|
|
{ nonce: "abc", returnTo: "/admin" },
|
|
SECRET,
|
|
600,
|
|
1_700_000_000,
|
|
);
|
|
expect(verifyOAuthState(token, SECRET, 1_700_000_100)).toEqual({
|
|
nonce: "abc",
|
|
returnTo: "/admin",
|
|
exp: 1_700_000_600,
|
|
});
|
|
});
|
|
|
|
it("binds state to an Organization and connection as one inseparable scope", () => {
|
|
const token = signOAuthState({
|
|
nonce: "scoped",
|
|
returnTo: "/admin",
|
|
organizationId: "org-acme",
|
|
connectionId: "connection-acme",
|
|
}, SECRET, 600, 1_700_000_000);
|
|
expect(verifyOAuthState(token, SECRET, 1_700_000_100)).toEqual({
|
|
nonce: "scoped",
|
|
returnTo: "/admin",
|
|
organizationId: "org-acme",
|
|
connectionId: "connection-acme",
|
|
exp: 1_700_000_600,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("sanitizeReturnTo", () => {
|
|
it("allows admin paths and rewrites legacy org slug prefixes", () => {
|
|
expect(sanitizeReturnTo("/admin")).toBe("/admin");
|
|
expect(sanitizeReturnTo("/admin/usage")).toBe("/admin/usage");
|
|
expect(sanitizeReturnTo("/admin/org/acme")).toBe("/admin");
|
|
expect(sanitizeReturnTo("/admin/org/acme/usage")).toBe("/admin/usage");
|
|
});
|
|
|
|
it("blocks open redirects", () => {
|
|
expect(sanitizeReturnTo("https://evil.example/")).toBe("/admin");
|
|
expect(sanitizeReturnTo("//evil.example")).toBe("/admin");
|
|
expect(sanitizeReturnTo("/api/me")).toBe("/admin");
|
|
expect(sanitizeReturnTo("")).toBe("/admin");
|
|
});
|
|
});
|