admin-token-opaque-fallback.test.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. import { beforeEach, describe, expect, it, vi } from "vitest";
  2. // Hoisted mocks
  3. const mockCookies = vi.hoisted(() => vi.fn());
  4. const mockHeaders = vi.hoisted(() => vi.fn());
  5. const mockGetEnvConfig = vi.hoisted(() => vi.fn());
  6. const mockValidateApiKeyAndGetUser = vi.hoisted(() => vi.fn());
  7. const mockFindKeyList = vi.hoisted(() => vi.fn());
  8. const mockReadSession = vi.hoisted(() => vi.fn());
  9. const mockCookieStore = vi.hoisted(() => ({
  10. get: vi.fn(),
  11. set: vi.fn(),
  12. delete: vi.fn(),
  13. }));
  14. const mockHeadersStore = vi.hoisted(() => ({
  15. get: vi.fn(),
  16. }));
  17. const mockConfig = vi.hoisted(() => ({
  18. auth: { adminToken: "test-admin-secret-token-12345" },
  19. }));
  20. vi.mock("next/headers", () => ({
  21. cookies: mockCookies,
  22. headers: mockHeaders,
  23. }));
  24. vi.mock("@/lib/config/env.schema", () => ({
  25. getEnvConfig: mockGetEnvConfig,
  26. }));
  27. vi.mock("@/repository/key", () => ({
  28. validateApiKeyAndGetUser: mockValidateApiKeyAndGetUser,
  29. findKeyList: mockFindKeyList,
  30. }));
  31. vi.mock("@/lib/auth-session-store/redis-session-store", () => ({
  32. RedisSessionStore: class {
  33. read = mockReadSession;
  34. create = vi.fn();
  35. revoke = vi.fn();
  36. rotate = vi.fn();
  37. },
  38. }));
  39. vi.mock("@/lib/logger", () => ({
  40. logger: { warn: vi.fn(), error: vi.fn(), info: vi.fn(), debug: vi.fn() },
  41. }));
  42. vi.mock("@/lib/config/config", () => ({
  43. config: mockConfig,
  44. }));
  45. function setSessionMode(mode: "legacy" | "dual" | "opaque") {
  46. mockGetEnvConfig.mockReturnValue({
  47. SESSION_TOKEN_MODE: mode,
  48. ENABLE_SECURE_COOKIES: false,
  49. });
  50. }
  51. function setAuthCookie(token?: string) {
  52. mockCookieStore.get.mockReturnValue(token ? { value: token } : undefined);
  53. }
  54. function setBearerHeader(token?: string) {
  55. mockHeadersStore.get.mockReturnValue(token ? `Bearer ${token}` : null);
  56. }
  57. describe("admin token opaque-mode fallback", () => {
  58. const ADMIN_TOKEN = "test-admin-secret-token-12345";
  59. beforeEach(() => {
  60. vi.resetModules();
  61. vi.clearAllMocks();
  62. mockCookies.mockResolvedValue(mockCookieStore);
  63. mockHeaders.mockResolvedValue(mockHeadersStore);
  64. mockHeadersStore.get.mockReturnValue(null);
  65. mockCookieStore.get.mockReturnValue(undefined);
  66. setSessionMode("opaque");
  67. mockReadSession.mockResolvedValue(null);
  68. mockFindKeyList.mockResolvedValue([]);
  69. mockValidateApiKeyAndGetUser.mockResolvedValue(null);
  70. mockConfig.auth.adminToken = ADMIN_TOKEN;
  71. });
  72. it("opaque mode + raw admin token via cookie -> auth succeeds", async () => {
  73. setAuthCookie(ADMIN_TOKEN);
  74. const { getSession } = await import("@/lib/auth");
  75. const session = await getSession();
  76. expect(session).not.toBeNull();
  77. expect(session!.user.id).toBe(-1);
  78. expect(session!.user.role).toBe("admin");
  79. expect(session!.key.name).toBe("ADMIN_TOKEN");
  80. });
  81. it("opaque mode + raw non-admin API key via cookie -> auth fails", async () => {
  82. setAuthCookie("sk-regular-user-key");
  83. // Even if this key is valid in DB, opaque mode must reject raw keys
  84. mockValidateApiKeyAndGetUser.mockResolvedValue({
  85. user: { id: 1, name: "user", role: "user", isEnabled: true },
  86. key: {
  87. id: 1,
  88. userId: 1,
  89. name: "key-1",
  90. key: "sk-regular-user-key",
  91. isEnabled: true,
  92. canLoginWebUi: true,
  93. },
  94. });
  95. const { getSession } = await import("@/lib/auth");
  96. const session = await getSession();
  97. expect(session).toBeNull();
  98. // Must NOT fall back to validateApiKeyAndGetUser for non-admin keys
  99. expect(mockValidateApiKeyAndGetUser).not.toHaveBeenCalled();
  100. });
  101. it("opaque mode + admin token via Bearer header -> auth succeeds", async () => {
  102. // No cookie set; use Authorization header instead
  103. setBearerHeader(ADMIN_TOKEN);
  104. const { getSession } = await import("@/lib/auth");
  105. const session = await getSession();
  106. expect(session).not.toBeNull();
  107. expect(session!.user.id).toBe(-1);
  108. expect(session!.user.role).toBe("admin");
  109. expect(session!.key.name).toBe("ADMIN_TOKEN");
  110. });
  111. it("opaque mode + valid opaque session -> auth succeeds (original logic unchanged)", async () => {
  112. const crypto = await import("node:crypto");
  113. const keyString = "sk-opaque-source-key";
  114. const fingerprint = `sha256:${crypto.createHash("sha256").update(keyString, "utf8").digest("hex")}`;
  115. setAuthCookie("sid_valid_session");
  116. mockReadSession.mockResolvedValue({
  117. sessionId: "sid_valid_session",
  118. keyFingerprint: fingerprint,
  119. userId: 42,
  120. userRole: "user",
  121. createdAt: Date.now() - 1000,
  122. expiresAt: Date.now() + 86400_000,
  123. });
  124. mockFindKeyList.mockResolvedValue([
  125. {
  126. id: 1,
  127. userId: 42,
  128. name: "key-1",
  129. key: keyString,
  130. isEnabled: true,
  131. canLoginWebUi: true,
  132. limit5hUsd: null,
  133. limitDailyUsd: null,
  134. dailyResetMode: "fixed",
  135. dailyResetTime: "00:00",
  136. limitWeeklyUsd: null,
  137. limitMonthlyUsd: null,
  138. limitTotalUsd: null,
  139. limitConcurrentSessions: 0,
  140. providerGroup: null,
  141. cacheTtlPreference: null,
  142. createdAt: new Date(),
  143. updatedAt: new Date(),
  144. },
  145. ]);
  146. mockValidateApiKeyAndGetUser.mockResolvedValue({
  147. user: {
  148. id: 42,
  149. name: "user-42",
  150. description: "test",
  151. role: "user",
  152. rpm: 100,
  153. dailyQuota: 100,
  154. providerGroup: null,
  155. tags: [],
  156. isEnabled: true,
  157. expiresAt: null,
  158. allowedClients: [],
  159. allowedModels: [],
  160. limit5hUsd: 0,
  161. limitWeeklyUsd: 0,
  162. limitMonthlyUsd: 0,
  163. limitTotalUsd: null,
  164. limitConcurrentSessions: 0,
  165. dailyResetMode: "fixed",
  166. dailyResetTime: "00:00",
  167. createdAt: new Date(),
  168. updatedAt: new Date(),
  169. },
  170. key: {
  171. id: 1,
  172. userId: 42,
  173. name: "key-1",
  174. key: keyString,
  175. isEnabled: true,
  176. canLoginWebUi: true,
  177. limit5hUsd: null,
  178. limitDailyUsd: null,
  179. dailyResetMode: "fixed",
  180. dailyResetTime: "00:00",
  181. limitWeeklyUsd: null,
  182. limitMonthlyUsd: null,
  183. limitTotalUsd: null,
  184. limitConcurrentSessions: 0,
  185. providerGroup: null,
  186. cacheTtlPreference: null,
  187. createdAt: new Date(),
  188. updatedAt: new Date(),
  189. },
  190. });
  191. const { getSession } = await import("@/lib/auth");
  192. const session = await getSession({ allowReadOnlyAccess: true });
  193. expect(session).not.toBeNull();
  194. expect(session!.user.id).toBe(42);
  195. });
  196. it("legacy mode -> behavior unchanged (admin token works via validateKey)", async () => {
  197. setSessionMode("legacy");
  198. setAuthCookie(ADMIN_TOKEN);
  199. const { getSession } = await import("@/lib/auth");
  200. const session = await getSession();
  201. expect(session).not.toBeNull();
  202. expect(session!.user.id).toBe(-1);
  203. expect(session!.user.role).toBe("admin");
  204. // Legacy mode should NOT touch opaque session store
  205. expect(mockReadSession).not.toHaveBeenCalled();
  206. });
  207. it("opaque mode + admin token not configured -> auth fails for raw token", async () => {
  208. mockConfig.auth.adminToken = "";
  209. setAuthCookie("some-random-token");
  210. const { getSession } = await import("@/lib/auth");
  211. const session = await getSession();
  212. expect(session).toBeNull();
  213. });
  214. });