1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-04 00:18:01 +01:00

Fix: prevent password reset email flooding (#2076)

* fix: prevent password reset email flooding

* feat: add tests to user service for password reset
This commit is contained in:
Tymoteusz Czech 2022-09-28 10:24:43 +02:00 committed by GitHub
parent 86824d693f
commit 0086f2f19f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 116 additions and 0 deletions

View File

@ -1,3 +1,4 @@
import { URL } from 'url';
import UserService from './user-service'; import UserService from './user-service';
import UserStoreMock from '../../test/fixtures/fake-user-store'; import UserStoreMock from '../../test/fixtures/fake-user-store';
import EventStoreMock from '../../test/fixtures/fake-event-store'; import EventStoreMock from '../../test/fixtures/fake-event-store';
@ -306,3 +307,108 @@ test('Should be a valid password with special chars', async () => {
expect(valid).toBe(true); expect(valid).toBe(true);
}); });
test('Should send password reset email if user exists', async () => {
const userStore = new UserStoreMock();
const eventStore = new EventStoreMock();
const accessService = new AccessServiceMock();
const resetTokenStore = new FakeResetTokenStore();
const resetTokenService = new ResetTokenService(
{ resetTokenStore },
config,
);
const emailService = new EmailService(config.email, config.getLogger);
const sessionStore = new FakeSessionStore();
const sessionService = new SessionService({ sessionStore }, config);
const settingService = new SettingService(
{
settingStore: new FakeSettingStore(),
eventStore: new FakeEventStore(),
},
config,
);
const service = new UserService({ userStore, eventStore }, config, {
accessService,
resetTokenService,
emailService,
sessionService,
settingService,
});
const unknownUser = service.createResetPasswordEmail('unknown@example.com');
expect(unknownUser).rejects.toThrowError('Could not find user');
await userStore.insert({
id: 123,
name: 'User',
username: 'Username',
email: 'known@example.com',
permissions: [],
imageUrl: '',
seenAt: new Date(),
loginAttempts: 0,
createdAt: new Date(),
isAPI: false,
generateImageUrl: () => '',
});
const knownUser = service.createResetPasswordEmail('known@example.com');
expect(knownUser).resolves.toBeInstanceOf(URL);
});
test('Should throttle password reset email', async () => {
const userStore = new UserStoreMock();
const eventStore = new EventStoreMock();
const accessService = new AccessServiceMock();
const resetTokenStore = new FakeResetTokenStore();
const resetTokenService = new ResetTokenService(
{ resetTokenStore },
config,
);
const emailService = new EmailService(config.email, config.getLogger);
const sessionStore = new FakeSessionStore();
const sessionService = new SessionService({ sessionStore }, config);
const settingService = new SettingService(
{
settingStore: new FakeSettingStore(),
eventStore: new FakeEventStore(),
},
config,
);
const service = new UserService({ userStore, eventStore }, config, {
accessService,
resetTokenService,
emailService,
sessionService,
settingService,
});
await userStore.insert({
id: 123,
name: 'User',
username: 'Username',
email: 'known@example.com',
permissions: [],
imageUrl: '',
seenAt: new Date(),
loginAttempts: 0,
createdAt: new Date(),
isAPI: false,
generateImageUrl: () => '',
});
jest.useFakeTimers();
const attempt1 = service.createResetPasswordEmail('known@example.com');
await expect(attempt1).resolves.toBeInstanceOf(URL);
const attempt2 = service.createResetPasswordEmail('known@example.com');
await expect(attempt2).resolves.toBe(undefined);
jest.runAllTimers();
const attempt3 = service.createResetPasswordEmail('known@example.com');
await expect(attempt3).resolves.toBeInstanceOf(URL);
});

View File

@ -76,6 +76,8 @@ class UserService {
private settingService: SettingService; private settingService: SettingService;
private passwordResetTimeouts: { [key: string]: NodeJS.Timeout } = {};
constructor( constructor(
stores: Pick<IUnleashStores, 'userStore' | 'eventStore'>, stores: Pick<IUnleashStores, 'userStore' | 'eventStore'>,
{ {
@ -400,11 +402,19 @@ class UserService {
if (!receiver) { if (!receiver) {
throw new NotFoundError(`Could not find ${receiverEmail}`); throw new NotFoundError(`Could not find ${receiverEmail}`);
} }
if (this.passwordResetTimeouts[receiver.id]) {
return;
}
const resetLink = await this.resetTokenService.createResetPasswordUrl( const resetLink = await this.resetTokenService.createResetPasswordUrl(
receiver.id, receiver.id,
user.username || user.email, user.username || user.email,
); );
this.passwordResetTimeouts[receiver.id] = setTimeout(() => {
delete this.passwordResetTimeouts[receiver.id];
}, 1000 * 60); // 1 minute
await this.emailService.sendResetMail( await this.emailService.sendResetMail(
receiver.name, receiver.name,
receiver.email, receiver.email,