diff --git a/README.md b/README.md index 740b6ca..321acf4 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,89 @@ app.register(cookie, { - An `Array` can be passed if key rotation is desired. Read more about it in [Rotating signing secret](#rotating-secret). - More sophisticated cookie signing mechanisms can be implemented by supplying an `Object`. Read more about it in [Custom cookie signer](#custom-cookie-signer). -- `parseOptions`: An `Object` to pass as options to [cookie parse](https://github.com/jshttp/cookie#cookieparsestr-options). +- `parseOptions`: An `Object` to modify the serialization of set cookies. + +#### parseOptions + +##### domain + +Specifies the value for the [`Domain` `Set-Cookie` attribute](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.3). By default, no +domain is set, and most clients will consider the cookie to apply to only the current domain. + +##### encode + +Specifies a function that will be used to encode a cookie's value. Since value of a cookie +has a limited character set (and must be a simple string), this function can be used to encode +a value into a string suited for a cookie's value. + +The default function is the global `encodeURIComponent`, which will encode a JavaScript string +into UTF-8 byte sequences and then URL-encode any that fall outside of the cookie range. + +##### expires + +Specifies the `Date` object to be the value for the [`Expires` `Set-Cookie` attribute](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.1). +By default, no expiration is set, and most clients will consider this a "non-persistent cookie" and +will delete it on a condition like exiting a web browser application. + +**Note:** the [cookie storage model specification](https://datatracker.ietf.org/doc/html/rfc6265#section-5.3) states that if both `expires` and +`maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients by obey this, +so if both are set, they should point to the same date and time. + +##### httpOnly + +Specifies the `boolean` value for the [`HttpOnly` `Set-Cookie` attribute](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.6). When truthy, +the `HttpOnly` attribute is set, otherwise it is not. By default, the `HttpOnly` attribute is not set. + +**Note:** be careful when setting this to `true`, as compliant clients will not allow client-side +JavaScript to see the cookie in `document.cookie`. + +##### maxAge + +Specifies the `number` (in seconds) to be the value for the [`Max-Age` `Set-Cookie` attribute](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2). +The given number will be converted to an integer by rounding down. By default, no maximum age is set. + +**Note:** the [cookie storage model specification](https://datatracker.ietf.org/doc/html/rfc6265#section-5.3) states that if both `expires` and +`maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients by obey this, +so if both are set, they should point to the same date and time. + +##### partitioned + +Specifies the `boolean` value for the [`Partitioned` `Set-Cookie`](https://datatracker.ietf.org/doc/html/draft-cutler-httpbis-partitioned-cookies#section-2.1) +attribute. When truthy, the `Partitioned` attribute is set, otherwise it is not. By default, the +`Partitioned` attribute is not set. + +⚠️ **Warning:** [This is an attribute that has not yet been fully standardized](https://github.com/fastify/fastify-cookie/pull/261#issuecomment-1803234334), and may change in the future without reflecting the semver versioning. This also means many clients may ignore the attribute until they understand it. + +More information about can be found in [the proposal](https://github.com/privacycg/CHIPS). + +##### path + +Specifies the value for the [`Path` `Set-Cookie` attribute](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.4). By default, the path +is considered the ["default path"](https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4). + +##### sameSite + +Specifies the `boolean` or `string` to be the value for the [`SameSite` `Set-Cookie` attribute](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-09#section-5.4.7). + + - `true` will set the `SameSite` attribute to `Strict` for strict same site enforcement. + - `false` will not set the `SameSite` attribute. + - `'lax'` will set the `SameSite` attribute to `Lax` for lax same site enforcement. + - `'none'` will set the `SameSite` attribute to `None` for an explicit cross-site cookie. + - `'strict'` will set the `SameSite` attribute to `Strict` for strict same site enforcement. + +More information about the different enforcement levels can be found in +[the specification](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-09#section-5.4.7). + +**Note:** This is an attribute that has not yet been fully standardized, and may change in the future. +This also means many clients may ignore this attribute until they understand it. + +##### secure + +Specifies the `boolean` value for the [`Secure` `Set-Cookie` attribute](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.5). When truthy, +the `Secure` attribute is set, otherwise it is not. By default, the `Secure` attribute is not set. + +**Note:** be careful when setting this to `true`, as compliant clients will not send the cookie back to +the server in the future if the browser does not have an HTTPS connection. ## API diff --git a/cookie.js b/cookie.js index 349db26..9a8a022 100644 --- a/cookie.js +++ b/cookie.js @@ -95,7 +95,7 @@ function parse (str, options) { const key = str.substring(pos, eqIdx++).trim() // only assign once - if (undefined === result[key]) { + if (result[key] === undefined) { const val = (str.charCodeAt(eqIdx) === 0x22) ? str.substring(eqIdx + 1, terminatorPos - 1).trim() : str.substring(eqIdx, terminatorPos).trim() @@ -183,6 +183,12 @@ function serialize (name, val, options) { str += '; Secure' } + // Draft implementation to support Chrome from 2024-Q1 forward. + // See https://datatracker.ietf.org/doc/html/draft-cutler-httpbis-partitioned-cookies#section-2.1 + if (opt.partitioned) { + str += '; Partitioned' + } + if (opt.sameSite) { const sameSite = typeof opt.sameSite === 'string' ? opt.sameSite.toLowerCase() diff --git a/test/cookie.test.js b/test/cookie.test.js index 767302a..b05faad 100644 --- a/test/cookie.test.js +++ b/test/cookie.test.js @@ -92,6 +92,46 @@ test('should set multiple cookies', (t) => { }) }) +test('should set multiple cookies', (t) => { + t.plan(12) + const fastify = Fastify() + fastify.register(plugin) + + fastify.get('/', (req, reply) => { + reply + .setCookie('foo', 'foo') + .cookie('bar', 'test', { + partitioned: true + }) + .setCookie('wee', 'woo', { + partitioned: true, + secure: true + }) + .send({ hello: 'world' }) + }) + + fastify.inject({ + method: 'GET', + url: '/' + }, (err, res) => { + t.error(err) + t.equal(res.statusCode, 200) + t.same(JSON.parse(res.body), { hello: 'world' }) + + const cookies = res.cookies + t.equal(cookies.length, 3) + t.equal(cookies[0].name, 'foo') + t.equal(cookies[0].value, 'foo') + t.equal(cookies[1].name, 'bar') + t.equal(cookies[1].value, 'test') + t.equal(cookies[2].name, 'wee') + t.equal(cookies[2].value, 'woo') + + t.equal(res.headers['set-cookie'][1], 'bar=test; Partitioned') + t.equal(res.headers['set-cookie'][2], 'wee=woo; Secure; Partitioned') + }) +}) + test('cookies get set correctly with millisecond dates', (t) => { t.plan(8) const fastify = Fastify() diff --git a/types/plugin.d.ts b/types/plugin.d.ts index 23e7500..d337675 100644 --- a/types/plugin.d.ts +++ b/types/plugin.d.ts @@ -125,9 +125,9 @@ declare namespace fastifyCookie { httpOnly?: boolean; /** A `number` in seconds that specifies the `Expires` attribute by adding the specified seconds to the current date. If both `expires` and `maxAge` are set, then `expires` is used. */ maxAge?: number; + partitioned?: boolean; /** The `Path` attribute. Defaults to `/` (the root path). */ path?: string; - priority?: "low" | "medium" | "high"; /** A `boolean` or one of the `SameSite` string attributes. E.g.: `lax`, `none` or `strict`. */ sameSite?: 'lax' | 'none' | 'strict' | boolean; /** The `boolean` value of the `Secure` attribute. Set this option to false when communicating over an unencrypted (HTTP) connection. Value can be set to `auto`; in this case the `Secure` attribute will be set to false for HTTP request, in case of HTTPS it will be set to true. Defaults to true. */ diff --git a/types/plugin.test-d.ts b/types/plugin.test-d.ts index 12ac223..9d8da4d 100644 --- a/types/plugin.test-d.ts +++ b/types/plugin.test-d.ts @@ -172,6 +172,7 @@ const parseOptions: fastifyCookieStar.CookieSerializeOptions = { sameSite: 'lax', secure: true, signed: true, + partitioned: false, }; expectType(parseOptions);