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 <ojeytonwilliams@gmail.com>
This commit is contained in:
Mrugesh Mohapatra
2025-06-25 19:43:44 +05:30
committed by GitHub
parent acd5508831
commit 6848da8320
2 changed files with 65 additions and 27 deletions

View File

@@ -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);
});
});
});

View File

@@ -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);