diff --git a/src/session.ts b/src/session.ts index b28ae1f..4694fa0 100644 --- a/src/session.ts +++ b/src/session.ts @@ -16,9 +16,8 @@ export default function session< const name = options.name || "sid" const store = options.store || new MemoryStore() const genId = options.genid || nanoid - const encode = options.encode const touchAfter = options.touchAfter ?? -1 - const cookieOpts = options.cookie || {} + const { unsign, ...cookieOpts } = options.cookie ?? {} function decorateSession(req: Req, res: Res, session: TypedSession, id: string, _now: number) { Object.defineProperties(session, { @@ -53,8 +52,13 @@ export default function session< const _now = Date.now() - let sessionId = req.cookies[name]?.value + const sessionCookie = req.cookies[name] + if (unsign != null && sessionCookie != null && !sessionCookie.signed) sessionCookie.unsign(unsign) + let sessionId: string | null = null + try { + sessionId = sessionCookie?.value ?? null + } catch (err) {} const _session = sessionId ? await store.get(sessionId) : null let session: TypedSession @@ -101,7 +105,7 @@ export default function session< res.registerLateHeaderAction(lateHeaderAction, (res: Res) => { if (!(session[isNew] && Object.keys(session).length > 1) && !session[isTouched] && !session[isDestroyed]) return - appendSessionCookieHeader(res, name, session, encode) + appendSessionCookieHeader(res, name, session, cookieOpts) }) return session diff --git a/src/types.ts b/src/types.ts index cea675b..eaf1135 100644 --- a/src/types.ts +++ b/src/types.ts @@ -38,12 +38,33 @@ export interface SessionStore { } export interface Options { - name?: string - store?: SessionStore - genid?: () => string - encode?: (rawSid: string) => string - decode?: (encryptedSid: string) => string | null - touchAfter?: number - cookie?: Partial> - autoCommit?: boolean + name?: string | undefined + store?: SessionStore | undefined + genid?: (() => string) | undefined + touchAfter?: number | undefined + cookie?: + | (Partial> & { + /** + * `otterhttp` cookie `sign` function, will be passed to `res.cookie`. + * @default undefined + */ + sign?: ((value: string) => string) | undefined + + /** + * `otterhttp` cookie 'unsign' function, will be used to unsign session cookies. + * + * You must ensure that encoded session cookies are not matched by your `otterhttp` `App`'s configured + * `signedCookieMatcher`. Otherwise, `otterhttp` will attempt to decode session cookies using the `App`'s configured + * `cookieUnsigner` instead, and unsigning with this function will not be attempted. + * @default undefined + */ + unsign?: ((signedValue: string) => string) | undefined + + /** + * `otterhttp` cookie 'encode' function, will be passed to `res.cookie`. + * @default undefined + */ + encode?: ((value: string) => string) | undefined + }) + | undefined } diff --git a/src/utils.ts b/src/utils.ts index 7449f55..0349a72 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,14 +2,11 @@ import type { Request, Response } from "@otterhttp/app" import type { Options, Session } from "./types" -export function appendSessionCookieHeader< - Req extends Request = Request, - Res extends Response = Response, ->( +export function appendSessionCookieHeader = Response>( res: Res, name: string, { cookie, id }: Pick, - encodeFn?: Options["encode"], + { encode, sign }: Pick, "encode" | "sign">, ) { if (res.headersSent) return res.cookie(name, id, { @@ -19,6 +16,7 @@ export function appendSessionCookieHeader< domain: cookie.domain, sameSite: cookie.sameSite, secure: cookie.secure, - encode: encodeFn, + encode, + sign, }) } diff --git a/test/session.test.ts b/test/session.test.ts index 7d54805..68aa452 100644 --- a/test/session.test.ts +++ b/test/session.test.ts @@ -63,7 +63,10 @@ describe("session()", () => { const cookie = { httpOnly: false, } - const sess = await session({ cookie })({} as Request, { registerLateHeaderAction: vi.fn() } as unknown as Response) + const sess = await session({ cookie })( + { cookies: {} } as Request, + { registerLateHeaderAction: vi.fn() } as unknown as Response, + ) expect(sess.cookie.httpOnly).toBeFalsy() }) @@ -282,16 +285,16 @@ describe("session()", () => { .end() }) }) - test("allow encode and decode sid", async () => { - const decode = (key: string) => { + test("allow sign and unsign sid", async () => { + const unsign = (key: string) => { if (key.startsWith("sig.")) return key.substring(4) - return null + throw new Error() } - const encode = (key: string) => { + const sign = (key: string) => { return `sig.${key}` } const store = new MemoryStore() - const sessionFn = session({ store, encode, decode }) + const sessionFn = session({ store, cookie: { sign, unsign } }) let sid: string | undefined const app = new App() app.use("/", async (req: Request, res: Response, next) => { @@ -317,10 +320,10 @@ describe("session()", () => { const res1 = await fetch("/first") expect(sid).toBeDefined() - expect(res1.headers.getSetCookie()).toContain(`sid=${encode(sid as string)}; Path=/; HttpOnly`) + expect(res1.headers.getSetCookie()).toContain(`sid=${sign(sid as string)}; Path=/; HttpOnly`) expect(store.store.has(sid as string)).toBe(true) - const res2 = await fetch("/second", { headers: { cookie: `sid=${encode(sid as string)}` } }) + const res2 = await fetch("/second", { headers: { cookie: `sid=${sign(sid as string)}` } }) await expect(res2.text()).resolves.toEqual("bar") const res3 = await fetch("/second", { headers: { cookie: `sid=${sid}` } })