/* * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ import { Inject, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { OptionsGraph, OptionsType, zxcvbnAsync, zxcvbnOptions, } from '@zxcvbn-ts/core'; import { adjacencyGraphs, dictionary as zxcvbnCommonDictionary, } from '@zxcvbn-ts/language-common'; import { dictionary as zxcvbnEnDictionary, translations as zxcvbnEnTranslations, } from '@zxcvbn-ts/language-en'; import { Repository } from 'typeorm'; import authConfiguration, { AuthConfig } from '../config/auth.config'; import { InvalidCredentialsError, NoLocalIdentityError, NotInDBError, PasswordTooWeakError, } from '../errors/errors'; import { ConsoleLoggerService } from '../logger/console-logger.service'; import { User } from '../users/user.entity'; import { checkPassword, hashPassword } from '../utils/password'; import { Identity } from './identity.entity'; import { ProviderType } from './provider-type.enum'; import { getFirstIdentityFromUser } from './utils'; @Injectable() export class IdentityService { constructor( private readonly logger: ConsoleLoggerService, @InjectRepository(Identity) private identityRepository: Repository, @Inject(authConfiguration.KEY) private authConfig: AuthConfig, ) { this.logger.setContext(IdentityService.name); const options: OptionsType = { dictionary: { ...zxcvbnCommonDictionary, ...zxcvbnEnDictionary, }, graphs: adjacencyGraphs as OptionsGraph, translations: zxcvbnEnTranslations, }; zxcvbnOptions.setOptions(options); } /** * @async * Retrieve an identity by userId and providerType. * @param {string} userId - the userId of the wanted identity * @param {ProviderType} providerType - the providerType of the wanted identity */ async getIdentityFromUserIdAndProviderType( userId: string, providerType: ProviderType, ): Promise { const identity = await this.identityRepository.findOne({ where: { providerUserId: userId, providerType: providerType, }, relations: ['user'], }); if (identity === null) { throw new NotInDBError(`Identity for user id '${userId}' not found`); } return identity; } /** * @async * Update the given Identity with the given information * @param {Identity} identity - the identity to update * @param {string | undefined} displayName - the displayName to update the user with * @param {string | undefined} email - the email to update the user with * @param {string | undefined} profilePicture - the profilePicture to update the user with */ async updateIdentity( identity: Identity, displayName?: string, email?: string, profilePicture?: string, ): Promise { if (identity.syncSource) { // The identity is the syncSource and the user should be changed accordingly const user = await identity.user; let shouldSave = false; if (displayName) { user.displayName = displayName; shouldSave = true; } if (email) { user.email = email; shouldSave = true; } if (profilePicture) { // ToDo: sync image (https://github.com/hedgedoc/hedgedoc/issues/5032) } if (shouldSave) { identity.user = Promise.resolve(user); return await this.identityRepository.save(identity); } } return identity; } /** * @async * Create a new generic identity. * @param {User} user - the user the identity should be added to * @param {ProviderType} providerType - the providerType of the identity * @param {string} userId - the userId the identity should have * @return {Identity} the new local identity */ async createIdentity( user: User, providerType: ProviderType, userId: string, ): Promise { const identity = Identity.create(user, providerType, false); identity.providerUserId = userId; return await this.identityRepository.save(identity); } /** * @async * Create a new identity for internal auth * @param {User} user - the user the identity should be added to * @param {string} password - the password the identity should have * @return {Identity} the new local identity */ async createLocalIdentity(user: User, password: string): Promise { const identity = Identity.create(user, ProviderType.LOCAL, false); identity.passwordHash = await hashPassword(password); return await this.identityRepository.save(identity); } /** * @async * Update the internal password of the specified the user * @param {User} user - the user, which identity should be updated * @param {string} newPassword - the new password * @throws {NoLocalIdentityError} the specified user has no internal identity * @return {Identity} the changed identity */ async updateLocalPassword( user: User, newPassword: string, ): Promise { const internalIdentity: Identity | undefined = await getFirstIdentityFromUser(user, ProviderType.LOCAL); if (internalIdentity === undefined) { this.logger.debug( `The user with the username ${user.username} does not have a internal identity.`, 'updateLocalPassword', ); throw new NoLocalIdentityError('This user has no internal identity.'); } await this.checkPasswordStrength(newPassword); internalIdentity.passwordHash = await hashPassword(newPassword); return await this.identityRepository.save(internalIdentity); } /** * @async * Checks if the user and password combination matches * @param {User} user - the user to use * @param {string} password - the password to use * @throws {InvalidCredentialsError} the password and user do not match * @throws {NoLocalIdentityError} the specified user has no internal identity */ async checkLocalPassword(user: User, password: string): Promise { const internalIdentity: Identity | undefined = await getFirstIdentityFromUser(user, ProviderType.LOCAL); if (internalIdentity === undefined) { this.logger.debug( `The user with the username ${user.username} does not have a internal identity.`, 'checkLocalPassword', ); throw new NoLocalIdentityError('This user has no internal identity.'); } if (!(await checkPassword(password, internalIdentity.passwordHash ?? ''))) { this.logger.debug( `Password check for ${user.username} did not succeed.`, 'checkLocalPassword', ); 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 */ async checkPasswordStrength(password: string): Promise { const result = await zxcvbnAsync(password); if (result.score < this.authConfig.local.minimalPasswordStrength) { throw new PasswordTooWeakError(); } } }