Skip to content

Commit

Permalink
feat: passthrough sign, unsign, encode options
Browse files Browse the repository at this point in the history
  • Loading branch information
Lordfirespeed committed Aug 27, 2024
1 parent 31a7d23 commit 69f50b1
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 26 deletions.
12 changes: 8 additions & 4 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
37 changes: 29 additions & 8 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Pick<Cookie, "maxAge" | "httpOnly" | "path" | "domain" | "secure" | "sameSite">>
autoCommit?: boolean
name?: string | undefined
store?: SessionStore | undefined
genid?: (() => string) | undefined
touchAfter?: number | undefined
cookie?:
| (Partial<Exclude<Cookie, "expires">> & {
/**
* `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
}
10 changes: 4 additions & 6 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Req> = Response<Req>,
>(
export function appendSessionCookieHeader<Req extends Request = Request, Res extends Response<Req> = Response<Req>>(
res: Res,
name: string,
{ cookie, id }: Pick<Session, "cookie" | "id">,
encodeFn?: Options["encode"],
{ encode, sign }: Pick<Exclude<Options["cookie"], undefined>, "encode" | "sign">,
) {
if (res.headersSent) return
res.cookie(name, id, {
Expand All @@ -19,6 +16,7 @@ export function appendSessionCookieHeader<
domain: cookie.domain,
sameSite: cookie.sameSite,
secure: cookie.secure,
encode: encodeFn,
encode,
sign,
})
}
19 changes: 11 additions & 8 deletions test/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
Expand Down Expand Up @@ -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<Request, Response>()
app.use("/", async (req: Request, res: Response, next) => {
Expand All @@ -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}` } })
Expand Down

0 comments on commit 69f50b1

Please sign in to comment.