fix: allow unauthenticated access to users.sendConfirmationEmail (#40702)

pull/40518/merge
Ricardo Garim 7 days ago committed by GitHub
parent 67b0a18edc
commit d7f19f8088
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/crisp-taxis-pay.md
  2. 12
      apps/meteor/app/api/server/v1/users.ts
  3. 24
      apps/meteor/server/methods/sendConfirmationEmail.ts
  4. 55
      apps/meteor/tests/end-to-end/api/users.ts
  5. 1
      packages/ddp-client/src/types/methods.ts

@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---
Fixes `users.sendConfirmationEmail` rejecting unauthenticated requests, which prevented unverified users from resending their verification email from the login screen

@ -40,6 +40,7 @@ import { regeneratePersonalAccessTokenOfUser } from '../../../../imports/persona
import { removePersonalAccessTokenOfUser } from '../../../../imports/personal-access-tokens/server/api/methods/removeToken';
import { UserChangedAuditStore } from '../../../../server/lib/auditServerEvents/userChanged';
import { i18n } from '../../../../server/lib/i18n';
import { SystemLogger } from '../../../../server/lib/logger/system';
import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey';
import { registerUser } from '../../../../server/methods/registerUser';
import { requestDataDownload } from '../../../../server/methods/requestDataDownload';
@ -1483,7 +1484,7 @@ API.v1
API.v1.post(
'users.sendConfirmationEmail',
{
authRequired: true,
authRequired: false,
body: isUsersSendConfirmationEmailParamsPOST,
rateLimiterOptions: {
numRequestsAllowed: 1,
@ -1492,16 +1493,11 @@ API.v1.post(
response: {
200: voidSuccessResponse,
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
},
},
async function action() {
const { email } = this.bodyParams;
if (await sendConfirmationEmail(email)) {
return API.v1.success();
}
return API.v1.failure();
void sendConfirmationEmail(this.bodyParams.email).catch((err) => SystemLogger.error({ msg: 'sendConfirmationEmail failed', err }));
return API.v1.success();
},
);

@ -1,7 +1,6 @@
import { Users } from '@rocket.chat/models';
import { Accounts } from 'meteor/accounts-base';
import { check } from 'meteor/check';
import { DDPRateLimiter } from 'meteor/ddp-rate-limiter';
import { Meteor } from 'meteor/meteor';
export const sendConfirmationEmail = async (to: string): Promise<boolean> => {
@ -9,14 +8,19 @@ export const sendConfirmationEmail = async (to: string): Promise<boolean> => {
const email = to.trim();
const user = await Users.findOneByEmailAddress(email, { projection: { _id: 1 } });
const user = await Users.findOneByEmailAddress(email, { projection: { emails: 1 } });
if (!user) {
return false;
}
// Do not re-send a verification mail to an address that is already verified.
const target = user.emails?.find((e) => e.address.toLowerCase() === email.toLowerCase() && !e.verified);
if (!target) {
return false;
}
try {
Accounts.sendVerificationEmail(user._id, email);
Accounts.sendVerificationEmail(user._id, target.address);
return true;
} catch (error: any) {
throw new Meteor.Error('error-email-send-failed', `Error trying to send email: ${error.message}`, {
@ -25,15 +29,3 @@ export const sendConfirmationEmail = async (to: string): Promise<boolean> => {
});
}
};
DDPRateLimiter.addRule(
{
type: 'method',
name: 'sendConfirmationEmail',
userId() {
return true;
},
},
5,
60000,
);

@ -4022,44 +4022,23 @@ describe('[Users]', () => {
});
describe('[/users.sendConfirmationEmail]', () => {
it('should send email to user (return success), when is a valid email', (done) => {
void request
.post(api('users.sendConfirmationEmail'))
.set(credentials)
.send({
email: adminEmail,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
})
.end(done);
});
it('should not send email to user(return error), when is a invalid email', (done) => {
void request
.post(api('users.sendConfirmationEmail'))
.set(credentials)
.send({
email: 'invalidEmail',
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
})
.end(done);
});
it('should return 401 when not authenticated', async () => {
await request
.post(api('users.sendConfirmationEmail'))
.expect('Content-Type', 'application/json')
.expect(401)
.expect((res: Response) => {
expect(res.body).to.have.property('status', 'error');
});
[
{ description: 'authenticated + known email', auth: true, email: adminEmail },
{ description: 'unauthenticated + known email', auth: false, email: adminEmail },
{ description: 'unauthenticated + unknown email', auth: false, email: 'nobody@example.invalid' },
].forEach(({ description, auth, email }) => {
it(`should return 200 success for ${description}`, async () => {
const req = request.post(api('users.sendConfirmationEmail')).send({ email });
if (auth) {
req.set(credentials);
}
await req
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
});
});
});
it('should return 400 when body is empty', async () => {

@ -1,7 +1,6 @@
// eslint-disable-next-line @typescript-eslint/naming-convention
export interface ServerMethods {
resetPassword(token: string, password: string): { token: string };
sendConfirmationEmail(to: string): boolean;
checkRegistrationSecretURL(hash: string): boolean;
}

Loading…
Cancel
Save