mirror of
https://github.com/documenso/documenso.git
synced 2026-10-03 18:23:54 +10:00
Selecting password auth failed with a generic "Unauthorized" for users who signed up via OAuth or passkey, with no way to set one. Detect the missing password and email the existing reset link from the signing dialog and security settings. Require a 2FA code and rate limit update-password.
114 lines
2.9 KiB
TypeScript
114 lines
2.9 KiB
TypeScript
import { SALT_ROUNDS } from '@documenso/lib/constants/auth';
|
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
import { prisma } from '@documenso/prisma';
|
|
import { compare, hash } from '@node-rs/bcrypt';
|
|
import { UserSecurityAuditLogType } from '@prisma/client';
|
|
|
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
|
import { jobsClient } from '../../jobs/client';
|
|
import { validateTwoFactorAuthentication } from '../2fa/validate-2fa';
|
|
|
|
export type UpdatePasswordOptions = {
|
|
userId: number;
|
|
password: string;
|
|
currentPassword: string;
|
|
totpCode?: string;
|
|
backupCode?: string;
|
|
requestMetadata?: RequestMetadata;
|
|
};
|
|
|
|
/**
|
|
* Update the password for a user who already has one.
|
|
*
|
|
* Requires the current password, and a valid TOTP or backup code if the user
|
|
* has two factor authentication enabled.
|
|
*/
|
|
export const updatePassword = async ({
|
|
userId,
|
|
password,
|
|
currentPassword,
|
|
totpCode,
|
|
backupCode,
|
|
requestMetadata,
|
|
}: UpdatePasswordOptions) => {
|
|
const user = await prisma.user.findFirstOrThrow({
|
|
where: {
|
|
id: userId,
|
|
},
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
password: true,
|
|
twoFactorEnabled: true,
|
|
twoFactorSecret: true,
|
|
twoFactorBackupCodes: true,
|
|
},
|
|
});
|
|
|
|
if (!user.password) {
|
|
throw new AppError(AppErrorCode.NO_PASSWORD);
|
|
}
|
|
|
|
const isCurrentPasswordValid = await compare(currentPassword, user.password);
|
|
if (!isCurrentPasswordValid) {
|
|
throw new AppError(AppErrorCode.INCORRECT_PASSWORD);
|
|
}
|
|
|
|
if (user.twoFactorEnabled) {
|
|
if (!totpCode && !backupCode) {
|
|
throw new AppError(AppErrorCode.TWO_FACTOR_MISSING_CREDENTIALS, { statusCode: 400 });
|
|
}
|
|
|
|
const isTwoFactorValid = await validateTwoFactorAuthentication({ user, totpCode, backupCode });
|
|
|
|
if (!isTwoFactorValid) {
|
|
throw new AppError(AppErrorCode.INCORRECT_TWO_FACTOR_CODE, { statusCode: 401 });
|
|
}
|
|
}
|
|
|
|
// Compare the new password with the old password
|
|
const isSamePassword = await compare(password, user.password);
|
|
if (isSamePassword) {
|
|
throw new AppError(AppErrorCode.SAME_PASSWORD);
|
|
}
|
|
|
|
const hashedNewPassword = await hash(password, SALT_ROUNDS);
|
|
|
|
const updatedUser = await prisma.$transaction(async (tx) => {
|
|
await tx.userSecurityAuditLog.create({
|
|
data: {
|
|
userId,
|
|
type: UserSecurityAuditLogType.PASSWORD_UPDATE,
|
|
userAgent: requestMetadata?.userAgent,
|
|
ipAddress: requestMetadata?.ipAddress,
|
|
},
|
|
});
|
|
|
|
await tx.passwordResetToken.deleteMany({
|
|
where: {
|
|
userId,
|
|
},
|
|
});
|
|
|
|
return await tx.user.update({
|
|
where: {
|
|
id: userId,
|
|
},
|
|
data: {
|
|
password: hashedNewPassword,
|
|
},
|
|
});
|
|
});
|
|
|
|
// Notify the user so a change made from a hijacked session does not go unnoticed.
|
|
await jobsClient.triggerJob({
|
|
name: 'send.password.reset.success.email',
|
|
payload: {
|
|
userId,
|
|
source: 'UPDATE',
|
|
},
|
|
});
|
|
|
|
return updatedUser;
|
|
};
|