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:
parent
86824d693f
commit
0086f2f19f
@ -1,3 +1,4 @@
|
||||
import { URL } from 'url';
|
||||
import UserService from './user-service';
|
||||
import UserStoreMock from '../../test/fixtures/fake-user-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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
@ -76,6 +76,8 @@ class UserService {
|
||||
|
||||
private settingService: SettingService;
|
||||
|
||||
private passwordResetTimeouts: { [key: string]: NodeJS.Timeout } = {};
|
||||
|
||||
constructor(
|
||||
stores: Pick<IUnleashStores, 'userStore' | 'eventStore'>,
|
||||
{
|
||||
@ -400,11 +402,19 @@ class UserService {
|
||||
if (!receiver) {
|
||||
throw new NotFoundError(`Could not find ${receiverEmail}`);
|
||||
}
|
||||
if (this.passwordResetTimeouts[receiver.id]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resetLink = await this.resetTokenService.createResetPasswordUrl(
|
||||
receiver.id,
|
||||
user.username || user.email,
|
||||
);
|
||||
|
||||
this.passwordResetTimeouts[receiver.id] = setTimeout(() => {
|
||||
delete this.passwordResetTimeouts[receiver.id];
|
||||
}, 1000 * 60); // 1 minute
|
||||
|
||||
await this.emailService.sendResetMail(
|
||||
receiver.name,
|
||||
receiver.email,
|
||||
|
Loading…
Reference in New Issue
Block a user