diff --git a/api/src/routes/protected/settings.test.ts b/api/src/routes/protected/settings.test.ts index e760ce72e14..6f9d304471e 100644 --- a/api/src/routes/protected/settings.test.ts +++ b/api/src/routes/protected/settings.test.ts @@ -25,6 +25,7 @@ import { getWaitMessage, validateSocialUrl } from './settings.js'; +import { findOrCreateUser } from '../helpers/auth-helpers.js'; const baseProfileUI = { isLocked: false, @@ -109,15 +110,28 @@ describe('settingRoutes', () => { '4kZFEVHChxzY7kX1XSzB4uhh8fcUwcqAGWV9hv25hsI6nviVlwzXCv2YE9lENYGY'; const tokenWithMissingUser = '4kZFEVHChxzY7kX1XSzB4uhh8fcUwcqAGWV9hv25hsI6nviVlwzXCv2YE9lENYGH'; + const tokenWithDifferentUser = + '4kZFEVHChxzY7kX1XSzB4uhh8fcUwcqAGWV9hv25hsI6nviVlwzXCv2YE9lENYGI'; const expiredToken = '4kZFEVHChxzY7kX1XSzB4uhh8fcUwcqAGWV9hv25hsI6nviVlwzXCv2YE9lENYGE'; - const tokens = [validToken, tokenWithMissingUser, expiredToken]; + const tokens = [ + validToken, + tokenWithMissingUser, + expiredToken, + tokenWithDifferentUser + ]; const newEmail = 'anything@goes.com'; + const otherUserEmail = 'another@user.com'; const encodedEmail = Buffer.from(newEmail).toString('base64'); const notEmail = Buffer.from('foobar.com').toString('base64'); beforeEach(async () => { + const otherUser = await findOrCreateUser( + fastifyTestInstance, + otherUserEmail + ); + await fastifyTestInstance.prisma.authToken.create({ data: { created: new Date(), @@ -137,6 +151,15 @@ describe('settingRoutes', () => { } }); + await fastifyTestInstance.prisma.authToken.create({ + data: { + created: new Date(), + id: tokenWithDifferentUser, + ttl: 1000, + userId: otherUser.id + } + }); + await fastifyTestInstance.prisma.authToken.create({ data: { created: new Date(Date.now() - 1000), @@ -157,6 +180,17 @@ describe('settingRoutes', () => { emailAuthLinkTTL: new Date() } }); + + // Simulate another user changing their email. This user is signed out. + await fastifyTestInstance.prisma.user.update({ + where: { id: otherUser.id }, + data: { + newEmail, + emailVerified: false, + emailVerifyTTL: new Date(), + emailAuthLinkTTL: new Date() + } + }); }); afterEach(async () => { @@ -167,6 +201,9 @@ describe('settingRoutes', () => { where: { id: defaultUserId }, data: { newEmail: null, email: defaultUserEmail, emailVerified: true } }); + await fastifyTestInstance.prisma.user.deleteMany({ + where: { email: otherUserEmail } + }); }); test('should reject requests without params', async () => { @@ -223,6 +260,20 @@ describe('settingRoutes', () => { expect(res.status).toBe(302); }); + test('should reject requests when the target user does not match the signed in user', async () => { + // The signed in user is the default (foo@bar.com), but the token is for + // a different user (another@user.com). + + const res = await superGet( + `/confirm-email?email=${encodedEmail}&token=${tokenWithDifferentUser}` + ); + + expect(res.headers.location).toBe( + `${HOME_LOCATION}?` + formatMessage(defaultErrorMessage) + ); + expect(res.status).toBe(302); + }); + // TODO(Post-MVP): there's no need to keep the auth token around if, // somehow, the user is missing test.todo( diff --git a/api/src/routes/protected/settings.ts b/api/src/routes/protected/settings.ts index 76407ae60ea..117aee9edb8 100644 --- a/api/src/routes/protected/settings.ts +++ b/api/src/routes/protected/settings.ts @@ -838,12 +838,15 @@ export const settingRedirectRoutes: FastifyPluginCallbackTypebox = ( return reply.redirectWithMessage(origin, expirationMessage); } - // TODO(Post-MVP): should this fail if it's not the currently signed in - // user? const targetUser = await fastify.prisma.user.findUnique({ where: { id: authToken.userId } }); + if (targetUser?.id !== req.user?.id) { + logger.warn('Target user does not match signed in user'); + return reply.redirectWithMessage(origin, redirectMessage); + } + if (targetUser?.newEmail !== email) { return reply.redirectWithMessage(origin, redirectMessage); }