From 6a56ce55412b0f22e49c70115622d0d2cb80466b Mon Sep 17 00:00:00 2001 From: Philip Molares Date: Sun, 25 Sep 2022 02:04:20 +0200 Subject: [PATCH] feat: check password strength for local login Signed-off-by: Philip Molares --- src/identity/identity.service.spec.ts | 12 ++++++++--- src/identity/identity.service.ts | 29 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/identity/identity.service.spec.ts b/src/identity/identity.service.spec.ts index 356670d36..7dfe4427e 100644 --- a/src/identity/identity.service.spec.ts +++ b/src/identity/identity.service.spec.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -13,6 +13,7 @@ import authConfigMock from '../config/mock/auth.config.mock'; import { InvalidCredentialsError, NoLocalIdentityError, + PasswordTooWeakError, } from '../errors/errors'; import { LoggerModule } from '../logger/logger.module'; import { User } from '../users/user.entity'; @@ -25,7 +26,7 @@ describe('IdentityService', () => { let service: IdentityService; let user: User; let identityRepo: Repository; - const password = 'test123'; + const password = 'AStrongPasswordToStartWith123'; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -81,7 +82,7 @@ describe('IdentityService', () => { user.identities = Promise.resolve([identity]); }); it('works', async () => { - const newPassword = 'newPassword'; + const newPassword = 'ThisIsAStrongNewP@ssw0rd'; const identity = await service.updateLocalPassword(user, newPassword); await checkPassword(newPassword, identity.passwordHash ?? '').then( (result) => expect(result).toBeTruthy(), @@ -94,6 +95,11 @@ describe('IdentityService', () => { NoLocalIdentityError, ); }); + it('fails, when new password is too weak', async () => { + await expect( + service.updateLocalPassword(user, 'password1'), + ).rejects.toThrow(PasswordTooWeakError); + }); }); describe('loginWithLocalIdentity', () => { diff --git a/src/identity/identity.service.ts b/src/identity/identity.service.ts index 647f92590..63c8bc0a2 100644 --- a/src/identity/identity.service.ts +++ b/src/identity/identity.service.ts @@ -5,6 +5,9 @@ */ import { Inject, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { zxcvbnAsync, zxcvbnOptions } from '@zxcvbn-ts/core'; +import zxcvbnCommonPackage from '@zxcvbn-ts/language-common'; +import zxcvbnEnPackage from '@zxcvbn-ts/language-en'; import { Repository } from 'typeorm'; import authConfiguration, { AuthConfig } from '../config/auth.config'; @@ -12,6 +15,7 @@ import { InvalidCredentialsError, NoLocalIdentityError, NotInDBError, + PasswordTooWeakError, } from '../errors/errors'; import { ConsoleLoggerService } from '../logger/console-logger.service'; import { User } from '../users/user.entity'; @@ -30,6 +34,15 @@ export class IdentityService { private authConfig: AuthConfig, ) { this.logger.setContext(IdentityService.name); + const options = { + dictionary: { + ...zxcvbnCommonPackage.dictionary, + ...zxcvbnEnPackage.dictionary, + }, + graphs: zxcvbnCommonPackage.adjacencyGraphs, + translations: zxcvbnEnPackage.translations, + }; + zxcvbnOptions.setOptions(options); } /** @@ -119,6 +132,7 @@ export class IdentityService { */ async createLocalIdentity(user: User, password: string): Promise { const identity = Identity.create(user, ProviderType.LOCAL, false); + await this.checkPasswordStrength(password); identity.passwordHash = await hashPassword(password); return await this.identityRepository.save(identity); } @@ -144,6 +158,7 @@ export class IdentityService { ); throw new NoLocalIdentityError('This user has no internal identity.'); } + await this.checkPasswordStrength(newPassword); internalIdentity.passwordHash = await hashPassword(newPassword); return await this.identityRepository.save(internalIdentity); } @@ -174,4 +189,18 @@ export class IdentityService { throw new InvalidCredentialsError('Password is not correct'); } } + + /** + * @async + * Check if the password is strong enough. + * This check is performed against the minimalPasswordStrength of the {@link AuthConfig}. + * @param {string} password - the password to check + * @throws {PasswordTooWeakError} the password is too weak + */ + private async checkPasswordStrength(password: string): Promise { + const result = await zxcvbnAsync(password); + if (result.score < this.authConfig.local.minimalPasswordStrength) { + throw new PasswordTooWeakError(); + } + } }