From 6848da8320fe4b57cd08e99a9233e6c81a0141d8 Mon Sep 17 00:00:00 2001 From: Mrugesh Mohapatra <1884376+raisedadead@users.noreply.github.com> Date: Wed, 25 Jun 2025 19:43:44 +0530 Subject: [PATCH] Merge commit from fork httpOnly (invisible to JS) and secure (https only) are now used. In order to update existing users without requiring them to re-authenticate, each request sets those properties on the cookie. Finally, the maxAge is now 30 days and is also updated on each request. i.e. it's a rolling 30 days. Co-authored-by: Oliver Eyton-Williams --- api/src/plugins/auth.test.ts | 69 ++++++++++++++++++++++++------------ api/src/plugins/auth.ts | 23 +++++++++--- 2 files changed, 65 insertions(+), 27 deletions(-) diff --git a/api/src/plugins/auth.test.ts b/api/src/plugins/auth.test.ts index e74bff8467f..2a0c95a9053 100644 --- a/api/src/plugins/auth.test.ts +++ b/api/src/plugins/auth.test.ts @@ -13,6 +13,8 @@ async function setupServer() { return fastify; } +const THIRTY_DAYS_IN_SECONDS = 2592000; + describe('auth', () => { let fastify: FastifyInstance; @@ -51,30 +53,11 @@ describe('auth', () => { path: '/', sameSite: 'Lax', domain: COOKIE_DOMAIN, - maxAge: token.ttl + maxAge: THIRTY_DAYS_IN_SECONDS, + httpOnly: true, + secure: true }); }); - - // TODO: Post-MVP sync the cookie max-age with the token ttl (i.e. the - // max-age should be the ttl/1000, not ttl) - it('should set the max-age of the cookie to match the ttl of the token', async () => { - const token = createAccessToken('test-id', 123000); - fastify.get('/test', async (req, reply) => { - reply.setAccessTokenCookie(token); - return { ok: true }; - }); - - const res = await fastify.inject({ - method: 'GET', - url: '/test' - }); - - expect(res.cookies[0]).toEqual( - expect.objectContaining({ - maxAge: 123000 - }) - ); - }); }); describe('authorize', () => { @@ -250,4 +233,46 @@ describe('auth', () => { expect(res.statusCode).toEqual(200); }); }); + + describe('onRequest Hook', () => { + it('should update the jwt_access_token to httpOnly and secure', async () => { + const rawValue = 'should-not-change'; + fastify.get('/test', (req, reply) => { + reply.send({ ok: true }); + }); + + const res = await fastify.inject({ + method: 'GET', + url: '/test', + cookies: { + jwt_access_token: signCookie(rawValue) + } + }); + + expect(res.cookies[0]).toMatchObject({ + httpOnly: true, + secure: true, + value: signCookie(rawValue), + maxAge: THIRTY_DAYS_IN_SECONDS + }); + + expect(res.json()).toStrictEqual({ ok: true }); + expect(res.statusCode).toBe(200); + }); + + it('should do nothing if there is no jwt_access_token', async () => { + fastify.get('/test', (req, reply) => { + reply.send({ ok: true }); + }); + + const res = await fastify.inject({ + method: 'GET', + url: '/test' + }); + + expect(res.cookies).toHaveLength(0); + expect(res.json()).toStrictEqual({ ok: true }); + expect(res.statusCode).toBe(200); + }); + }); }); diff --git a/api/src/plugins/auth.ts b/api/src/plugins/auth.ts index 0212c9e220d..4829d85d1af 100644 --- a/api/src/plugins/auth.ts +++ b/api/src/plugins/auth.ts @@ -28,13 +28,26 @@ declare module 'fastify' { } const auth: FastifyPluginCallback = (fastify, _options, done) => { + const cookieOpts = { + httpOnly: true, + secure: true, + maxAge: 2592000 // thirty days in seconds + }; fastify.decorateReply('setAccessTokenCookie', function (accessToken: Token) { const signedToken = jwt.sign({ accessToken }, JWT_SECRET); - void this.setCookie('jwt_access_token', signedToken, { - httpOnly: false, - secure: false, - maxAge: accessToken.ttl - }); + void this.setCookie('jwt_access_token', signedToken, cookieOpts); + }); + + // update existing jwt_access_token cookie properties + fastify.addHook('onRequest', (req, reply, done) => { + const rawCookie = req.cookies['jwt_access_token']; + if (rawCookie) { + const jwtAccessToken = req.unsignCookie(rawCookie); + if (jwtAccessToken.valid) { + reply.setCookie('jwt_access_token', jwtAccessToken.value, cookieOpts); + } + } + done(); }); fastify.decorateRequest('accessDeniedMessage', null);