feat(api): add plugin allowing server to update cookies (#55395)

This commit is contained in:
Oliver Eyton-Williams
2024-07-09 09:33:04 +02:00
committed by GitHub
parent 9e31e53ec7
commit bb95e2ff54
3 changed files with 152 additions and 0 deletions

View File

@@ -0,0 +1,113 @@
import Fastify, { FastifyInstance } from 'fastify';
import cookies, { type CookieSerializeOptions, sign } from './cookies';
import { cookieUpdate } from './cookie-update';
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
jest.mock('../utils/env', () => ({
...jest.requireActual('../utils/env'),
COOKIE_DOMAIN: 'www.example.com',
FREECODECAMP_NODE_ENV: 'not-development'
}));
describe('Cookie updates', () => {
let fastify: FastifyInstance;
const setup = async (attributes: CookieSerializeOptions) => {
// Since register creates a new scope, we need to create a route inside the
// scope for the plugin to be applied.
await fastify.register(cookieUpdate, fastify => {
// eslint-disable-next-line @typescript-eslint/require-await
fastify.get('/', async () => {
return { hello: 'world' };
});
return {
cookies: ['cookie_name'],
attributes
};
});
};
beforeEach(async () => {
fastify = Fastify();
await fastify.register(cookies);
});
afterEach(async () => {
await fastify.close();
});
it('should not set cookies that are not in the request', async () => {
await setup({});
const res = await fastify.inject({
method: 'GET',
url: '/',
headers: {
cookie: 'cookie_name_two=cookie_value'
}
});
expect(res.headers['set-cookie']).toBeUndefined();
});
it("should update the cookie's attributes without changing the value", async () => {
await setup({ sameSite: 'strict' });
const signedCookie = sign('cookie_value');
const encodedCookie = encodeURIComponent(signedCookie);
const res = await fastify.inject({
method: 'GET',
url: '/',
headers: {
cookie: `cookie_name=${signedCookie}`
}
});
const updatedCookie = res.headers['set-cookie'] as string;
expect(updatedCookie).toEqual(
expect.stringContaining(`cookie_name=${encodedCookie}`)
);
expect(updatedCookie).toEqual(expect.stringContaining('SameSite=Strict'));
});
it('should unsign the cookie if required', async () => {
await setup({ signed: false });
const signedCookie = sign('cookie_value');
const res = await fastify.inject({
method: 'GET',
url: '/',
headers: {
cookie: `cookie_name=${signedCookie}`
}
});
const updatedCookie = res.headers['set-cookie'] as string;
expect(updatedCookie).toEqual(
expect.stringContaining('cookie_name=cookie_value')
);
});
it('should respect the default cookie config if not overriden', async () => {
await setup({});
const res = await fastify.inject({
method: 'GET',
url: '/',
headers: {
cookie: 'cookie_name=anything'
}
});
expect(res.cookies[0]).toStrictEqual({
domain: 'www.example.com',
httpOnly: true,
name: 'cookie_name',
path: '/',
sameSite: 'Lax',
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
value: expect.any(String),
secure: true
});
});
});

View File

@@ -0,0 +1,37 @@
import { FastifyPluginCallback } from 'fastify';
import type { CookieSerializeOptions } from './cookies';
type Options = { cookies: string[]; attributes: CookieSerializeOptions };
/**
* Plugin that updates the attributes of cookies in the response, without
* changing the value.
*
* @param fastify The Fastify instance.
* @param options Options passed to the plugin via `fastify.register(plugin,
* options)`.
* @param options.cookies The names of the cookies to update.
* @param options.attributes The attributes to update the cookies with. NOTE:
* The attributes are merged with the default values given to \@fastify/cookie.
* @param done Callback to signal that the logic has completed.
*/
export const cookieUpdate: FastifyPluginCallback<Options> = (
fastify,
options,
done
) => {
fastify.addHook('onSend', (request, reply, _payload, next) => {
for (const cookie of options.cookies) {
const oldCookie = request.cookies[cookie];
if (!oldCookie) continue;
const unsigned = reply.unsignCookie(oldCookie);
const raw = unsigned.valid ? unsigned.value : oldCookie;
void reply.setCookie(cookie, raw, options.attributes);
}
next();
});
done();
};

View File

@@ -8,6 +8,8 @@ import {
FREECODECAMP_NODE_ENV
} from '../utils/env';
export { type CookieSerializeOptions } from '@fastify/cookie';
/**
* Signs a cookie value by prefixing it with "s:" and using the COOKIE_SECRET.
*