From 75917df14a7d16db352e6fcb30e4e873b1ec0827 Mon Sep 17 00:00:00 2001 From: Sem Bauke Date: Fri, 27 Feb 2026 21:25:55 +0100 Subject: [PATCH] fix(api): handle unauthenticated users in get-session-user endpoint (#65652) --- api/src/app.ts | 15 ++++++++------- api/src/routes/protected/user.test.ts | 13 ++++++++++++- api/src/routes/protected/user.ts | 16 ++++++++++++---- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/api/src/app.ts b/api/src/app.ts index 737c80587cf..1ea1adad2b3 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -191,13 +191,6 @@ export const build = async ( await fastify.register(protectedRoutes.userRoutes); }); - // CSRF protection disabled: - await fastify.register(async function (fastify, _opts) { - fastify.addHook('onRequest', fastify.send401IfNoUser); - - await fastify.register(protectedRoutes.userGetRoutes); - }); - // Routes that redirect if access is denied: await fastify.register(async function (fastify, _opts) { fastify.addHook('onRequest', fastify.redirectIfNoUser); @@ -209,6 +202,14 @@ export const build = async ( // TODO: The route should not handle its own AuthZ await fastify.register(protectedRoutes.challengeTokenRoutes); + // CSRF protection disabled: + // Routes that work for both authenticated and unauthenticated users: + void fastify.register(async function (fastify) { + fastify.addHook('onRequest', fastify.authorize); + + await fastify.register(protectedRoutes.userGetRoutes); + }); + // Routes for signed out users: void fastify.register(async function (fastify) { fastify.addHook('onRequest', fastify.authorize); diff --git a/api/src/routes/protected/user.test.ts b/api/src/routes/protected/user.test.ts index 1c73232aaad..6eb650294c7 100644 --- a/api/src/routes/protected/user.test.ts +++ b/api/src/routes/protected/user.test.ts @@ -1607,7 +1607,6 @@ Thanks and regards, { path: `/users/${otherUserId}`, method: 'DELETE' }, { path: '/account/delete', method: 'POST' }, { path: '/account/reset-progress', method: 'POST' }, - { path: '/user/get-session-user', method: 'GET' }, { path: '/user/user-token', method: 'DELETE' }, { path: '/user/user-token', method: 'POST' }, { path: '/user/ms-username', method: 'DELETE' }, @@ -1625,6 +1624,18 @@ Thanks and regards, expect(response.statusCode).toBe(401); }); }); + + describe('/user/get-session-user', () => { + test('GET returns 200 with empty user object for unauthenticated users', async () => { + const response = await superRequest('/user/get-session-user', { + method: 'GET', + setCookies + }); + + expect(response.statusCode).toBe(200); + expect(response.body).toStrictEqual({ user: {}, result: '' }); + }); + }); }); }); diff --git a/api/src/routes/protected/user.ts b/api/src/routes/protected/user.ts index 65cc4a5acbe..6bf3fca1c47 100644 --- a/api/src/routes/protected/user.ts +++ b/api/src/routes/protected/user.ts @@ -661,13 +661,21 @@ export const userGetRoutes: FastifyPluginCallbackTypebox = ( // This is one of the most requested routes. To avoid spamming the logs // with this route, we'll log requests at the debug level. logger.debug({ userId: req.user?.id }); + + // Handle unauthenticated users - this is not an error, it's how the client + // determines if they are signed in or not + if (!req.user?.id) { + logger.debug('Unauthenticated user requested session'); + return { user: {}, result: '' }; + } + try { const userTokenP = fastify.prisma.userToken.findFirst({ - where: { userId: req.user!.id } + where: { userId: req.user.id } }); const userP = fastify.prisma.user.findUnique({ - where: { id: req.user!.id }, + where: { id: req.user.id }, select: { about: true, acceptedPrivacyTerms: true, @@ -738,11 +746,11 @@ export const userGetRoutes: FastifyPluginCallbackTypebox = ( }); const completedSurveysP = fastify.prisma.survey.findMany({ - where: { userId: req.user!.id } + where: { userId: req.user.id } }); const msUsernameP = fastify.prisma.msUsername.findFirst({ - where: { userId: req.user?.id } + where: { userId: req.user.id } }); const [userToken, user, completedSurveys, msUsername] =