forked from bai/curriculum-project-hub
feat(hub): add Feishu OAuth session for org admin
Introduce signed cookie sessions, Feishu web OAuth, requireOrgRole guards, and the first org admin APIs (summary + project settings).
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
describe("oauth state signing", () => {
|
||||
it("round-trips state with returnTo", () => {
|
||||
const token = signOAuthState(
|
||||
{ nonce: "abc", returnTo: "/admin/org/acme" },
|
||||
SECRET,
|
||||
600,
|
||||
1_700_000_000,
|
||||
);
|
||||
expect(verifyOAuthState(token, SECRET, 1_700_000_100)).toEqual({
|
||||
nonce: "abc",
|
||||
returnTo: "/admin/org/acme",
|
||||
exp: 1_700_000_600,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("sanitizeReturnTo", () => {
|
||||
it("allows admin paths", () => {
|
||||
expect(sanitizeReturnTo("/admin/org/acme")).toBe("/admin/org/acme");
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user