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 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);
|
||||||
|
});
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user