From d798f8abfba005b4f06c6d86a2cd5fd64e2d1d22 Mon Sep 17 00:00:00 2001 From: adamjmcgrath Date: Thu, 18 Feb 2021 11:23:40 +0000 Subject: [PATCH 1/2] cookie chunk size should be browser max minus cookie attribute's length --- src/auth0-session/cookie-store.ts | 12 +++++++----- tests/auth0-session/cookie-store.test.ts | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/auth0-session/cookie-store.ts b/src/auth0-session/cookie-store.ts index 971dca010..9525062ce 100644 --- a/src/auth0-session/cookie-store.ts +++ b/src/auth0-session/cookie-store.ts @@ -5,11 +5,11 @@ import { encryption as deriveKey } from './utils/hkdf'; import createDebug from './utils/debug'; import { getAll as getCookies, clear as clearCookie, set as setCookie } from './utils/cookies'; import { Config } from './config'; -import { CookieSerializeOptions } from 'cookie'; +import { CookieSerializeOptions, serialize } from 'cookie'; const debug = createDebug('cookie-store'); const epoch = (): number => (Date.now() / 1000) | 0; // eslint-disable-line no-bitwise -const CHUNK_BYTE_SIZE = 4000; +const MAX_COOKIE_SIZE = 4096; const alg = 'dir'; const enc = 'A256GCM'; @@ -172,11 +172,13 @@ export default class CookieStore { debug('found session, creating signed session cookie(s) with name %o(.i)', sessionName); const value = this.encrypt(JSON.stringify(session), { iat, uat, exp }); - const chunkCount = Math.ceil(value.length / CHUNK_BYTE_SIZE); + const emptyCookie = serialize(`${sessionName}.0`, '', cookieOptions); + const chunkSize = MAX_COOKIE_SIZE - emptyCookie.length; + const chunkCount = Math.ceil(value.length / chunkSize); if (chunkCount > 1) { - debug('cookie size greater than %d, chunking', CHUNK_BYTE_SIZE); + debug('cookie size greater than %d, chunking', chunkSize); for (let i = 0; i < chunkCount; i++) { - const chunkValue = value.slice(i * CHUNK_BYTE_SIZE, (i + 1) * CHUNK_BYTE_SIZE); + const chunkValue = value.slice(i * chunkSize, (i + 1) * chunkSize); const chunkCookieName = `${sessionName}.${i}`; setCookie(res, chunkCookieName, chunkValue, cookieOptions); } diff --git a/tests/auth0-session/cookie-store.test.ts b/tests/auth0-session/cookie-store.test.ts index caed20714..7948c8fe9 100644 --- a/tests/auth0-session/cookie-store.test.ts +++ b/tests/auth0-session/cookie-store.test.ts @@ -101,6 +101,25 @@ describe('CookieStore', () => { expect(session.claims).toHaveProperty('big_claim'); }); + it('should limit total cookie size to 4096 Bytes', async () => { + const path = + '/some-really-really-really-really-really-really-really-really-really-really-really-really-really-long-path'; + const baseURL = await setup({ ...defaultConfig, session: { cookie: { path } } }); + const appSession = encrypted({ + big_claim: randomBytes(5000).toString('base64') + }); + expect(appSession.length).toBeGreaterThan(4096); + const cookieJar = toCookieJar({ appSession }, baseURL); + await get(baseURL, '/session', { cookieJar }); + const cookies: { [key: string]: string } = cookieJar + .getCookiesSync(`${baseURL}${path}`) + .reduce((obj, value) => Object.assign(obj, { [value.key]: value + '' }), {}); + expect(cookies['appSession.0']).toHaveLength(4096); + expect(cookies['appSession.1']).toHaveLength(4096); + expect(cookies['appSession.2']).toHaveLength(4096); + expect(cookies['appSession.3']).toHaveLength(1568); + }); + it('should handle unordered chunked cookies', async () => { const baseURL = await setup(defaultConfig); const appSession = encrypted({ sub: '__chunked_sub__' }); From d5b0d66d8d6e51da06b98b126cb84f28607d9ae1 Mon Sep 17 00:00:00 2001 From: adamjmcgrath Date: Thu, 18 Feb 2021 11:42:51 +0000 Subject: [PATCH 2/2] don't measure an empty cookie on every request --- src/auth0-session/cookie-store.ts | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/auth0-session/cookie-store.ts b/src/auth0-session/cookie-store.ts index 9525062ce..780f1461a 100644 --- a/src/auth0-session/cookie-store.ts +++ b/src/auth0-session/cookie-store.ts @@ -20,6 +20,8 @@ export default class CookieStore { private currentKey: JWK.OctKey | undefined; + private chunkSize: number; + constructor(public config: Config) { const secrets = Array.isArray(config.secret) ? config.secret : [config.secret]; this.keystore = new JWKS.KeyStore(); @@ -31,6 +33,20 @@ export default class CookieStore { } this.keystore.add(key); }); + + const { + cookie: { transient, ...cookieConfig }, + name: sessionName + } = this.config.session; + const cookieOptions: CookieSerializeOptions = { + ...cookieConfig + }; + if (!transient) { + cookieOptions.expires = new Date(); + } + + const emptyCookie = serialize(`${sessionName}.0`, '', cookieOptions); + this.chunkSize = MAX_COOKIE_SIZE - emptyCookie.length; } private encrypt(payload: string, headers: { [key: string]: any }): string { @@ -172,13 +188,11 @@ export default class CookieStore { debug('found session, creating signed session cookie(s) with name %o(.i)', sessionName); const value = this.encrypt(JSON.stringify(session), { iat, uat, exp }); - const emptyCookie = serialize(`${sessionName}.0`, '', cookieOptions); - const chunkSize = MAX_COOKIE_SIZE - emptyCookie.length; - const chunkCount = Math.ceil(value.length / chunkSize); + const chunkCount = Math.ceil(value.length / this.chunkSize); if (chunkCount > 1) { - debug('cookie size greater than %d, chunking', chunkSize); + debug('cookie size greater than %d, chunking', this.chunkSize); for (let i = 0; i < chunkCount; i++) { - const chunkValue = value.slice(i * chunkSize, (i + 1) * chunkSize); + const chunkValue = value.slice(i * this.chunkSize, (i + 1) * this.chunkSize); const chunkCookieName = `${sessionName}.${i}`; setCookie(res, chunkCookieName, chunkValue, cookieOptions); }