diff --git a/lib/main.test.ts b/lib/main.test.ts index 1b932cad..a8e6be68 100644 --- a/lib/main.test.ts +++ b/lib/main.test.ts @@ -48,6 +48,7 @@ describe("index exports", () => { "mapLoginMethodParamsForUrl", "sanitizeUrl", "exchangeAuthCode", + "getCookieOptions", "isAuthenticated", "isTokenExpired", "refreshToken", diff --git a/lib/main.ts b/lib/main.ts index f774b473..e2645eaa 100644 --- a/lib/main.ts +++ b/lib/main.ts @@ -24,7 +24,9 @@ export { sessionManagerActivityProxy, isClient, isServer, + getCookieOptions, } from "./utils"; +export type { CookieOptions } from "./utils"; export { getClaim, diff --git a/lib/utils/getCookieOptions.test.ts b/lib/utils/getCookieOptions.test.ts new file mode 100644 index 00000000..71117340 --- /dev/null +++ b/lib/utils/getCookieOptions.test.ts @@ -0,0 +1,74 @@ +import { describe, it, expect } from "vitest"; + +import { + getCookieOptions, + TWENTY_NINE_DAYS, + GLOBAL_COOKIE_OPTIONS, +} from "./getCookieOptions"; +import { storageSettings } from "../sessionManager/index"; + +describe("getCookieOptions", () => { + it("returns the default configuration when no options provided", () => { + const result = getCookieOptions(); + + expect(result).toMatchObject({ + maxAge: TWENTY_NINE_DAYS, + maxCookieLength: storageSettings.maxLength, + sameSite: "lax", + httpOnly: true, + path: "/", + }); + }); + + it("allows consumers to override default options", () => { + const result = getCookieOptions({ + secure: true, + sameSite: "none", + path: "/custom", + maxAge: 60, + domain: "example.com", + }); + + expect(result.secure).toBe(true); + expect(result.sameSite).toBe("none"); + expect(result.path).toBe("/custom"); + expect(result.maxAge).toBe(60); + expect(result.domain).toBe("example.com"); + }); + + it("preserves GLOBAL_COOKIE_OPTIONS when not overridden", () => { + const result = getCookieOptions({ domain: "test.com" }); + + expect(result.httpOnly).toBe(GLOBAL_COOKIE_OPTIONS.httpOnly); + expect(result.sameSite).toBe(GLOBAL_COOKIE_OPTIONS.sameSite); + expect(result.path).toBe(GLOBAL_COOKIE_OPTIONS.path); + expect(result.maxAge).toBe(GLOBAL_COOKIE_OPTIONS.maxAge); + expect(result.maxCookieLength).toBe(GLOBAL_COOKIE_OPTIONS.maxCookieLength); + expect(result.domain).toBe("test.com"); + }); + + it("user options take precedence over defaults", () => { + const result = getCookieOptions({ + httpOnly: false, + maxAge: 1000, + }); + + expect(result.httpOnly).toBe(false); + expect(result.maxAge).toBe(1000); + // Other defaults remain + expect(result.sameSite).toBe("lax"); + expect(result.path).toBe("/"); + }); +}); + +describe("GLOBAL_COOKIE_OPTIONS", () => { + it("contains secure defaults", () => { + expect(GLOBAL_COOKIE_OPTIONS.httpOnly).toBe(true); + expect(GLOBAL_COOKIE_OPTIONS.sameSite).toBe("lax"); + expect(GLOBAL_COOKIE_OPTIONS.path).toBe("/"); + expect(GLOBAL_COOKIE_OPTIONS.maxAge).toBe(TWENTY_NINE_DAYS); + expect(GLOBAL_COOKIE_OPTIONS.maxCookieLength).toBe( + storageSettings.maxLength, + ); + }); +}); diff --git a/lib/utils/getCookieOptions.ts b/lib/utils/getCookieOptions.ts new file mode 100644 index 00000000..fbf4c5e7 --- /dev/null +++ b/lib/utils/getCookieOptions.ts @@ -0,0 +1,59 @@ +import { storageSettings } from "../sessionManager/index.js"; + +export interface CookieOptions { + maxAge?: number; + domain?: string; + maxCookieLength?: number; + sameSite?: string; + httpOnly?: boolean; + secure?: boolean; + path?: string; +} + +export const TWENTY_NINE_DAYS = 2505600; + +/** + * Default cookie options used across Kinde SDKs. + * + * **Security Note:** The `secure` flag is intentionally omitted to support: + * - Framework-agnostic usage across different environments + * - Local development over HTTP (localhost) + * + * Warning: For production deployments using HTTPS, consumers must explicitly + * set `secure: true` via `getCookieOptions({ secure: true })` to ensure + * cookies are only transmitted over secure connections. + */ +export const GLOBAL_COOKIE_OPTIONS: CookieOptions = { + maxAge: TWENTY_NINE_DAYS, + maxCookieLength: storageSettings.maxLength, + sameSite: "lax", + httpOnly: true, + path: "/", +}; + +/** + * Returns cookie options by merging provided options with secure defaults. + * + * @param options - Custom cookie options to override defaults + * @returns Merged cookie options with GLOBAL_COOKIE_OPTIONS as base + * + * @example + * ```typescript + * // Development (HTTP) + * const devOptions = getCookieOptions(); + * + * // Production (HTTPS) - must set secure: true + * const prodOptions = getCookieOptions({ secure: true, domain: ".example.com" }); + * ``` + * + * **Security Warning:** Always set `secure: true` in production environments + * using HTTPS to prevent cookie transmission over insecure connections. + */ +export const getCookieOptions = ( + options: CookieOptions = {}, +): CookieOptions => { + return { + ...GLOBAL_COOKIE_OPTIONS, + ...options, + }; +}; diff --git a/lib/utils/index.ts b/lib/utils/index.ts index aca17c26..4cbc9323 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -9,6 +9,8 @@ export { frameworkSettings, generateKindeSDKHeader, } from "./exchangeAuthCode"; +export { getCookieOptions } from "./getCookieOptions"; +export type { CookieOptions } from "./getCookieOptions"; export { checkAuth } from "./checkAuth"; export { isCustomDomain } from "./isCustomDomain"; export { setRefreshTimer, clearRefreshTimer } from "./refreshTimer";