Password Handling
Overview
The starter kit implements secure password handling using industry best practices. This includes secure storage, validation, and recovery mechanisms.
Password Hashing
Passwords are hashed using bcrypt with a configurable work factor:
@Injectable()
export class PasswordService {
constructor(private configService: ConfigService) {}
private get saltRounds(): number {
return this.configService.get<number>('BCRYPT_SALT_ROUNDS', 12);
}
async hash(password: string): Promise<string> {
return hash(password, this.saltRounds);
}
async compare(password: string, hashedPassword: string): Promise<boolean> {
return compare(password, hashedPassword);
}
}
Configuration
Set the bcrypt salt rounds in your .env
file:
BCRYPT_SALT_ROUNDS=12
Higher values provide better security but slower performance.
Implementation in User Entity
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
email: string;
@Column()
password: string;
// Password reset fields
@Column({ nullable: true })
resetPasswordToken: string;
@Column({ nullable: true })
resetPasswordExpires: Date;
// Method to validate password
async validatePassword(password: string): Promise<boolean> {
const passwordService = Container.get(PasswordService);
return passwordService.compare(password, this.password);
}
}
Password Policy Enforcement
The starter kit includes a customizable password policy:
@Injectable()
export class PasswordPolicyService {
validatePassword(password: string): { valid: boolean; message?: string } {
if (password.length < 8) {
return {
valid: false,
message: 'Password must be at least 8 characters long',
};
}
if (!/[A-Z]/.test(password)) {
return {
valid: false,
message: 'Password must contain at least one uppercase letter',
};
}
if (!/[a-z]/.test(password)) {
return {
valid: false,
message: 'Password must contain at least one lowercase letter',
};
}
if (!/[0-9]/.test(password)) {
return {
valid: false,
message: 'Password must contain at least one number',
};
}
if (!/[^A-Za-z0-9]/.test(password)) {
return {
valid: false,
message: 'Password must contain at least one special character',
};
}
return { valid: true };
}
}
Password Validation Pipe
The starter kit includes a validation pipe for password fields:
@Injectable()
export class PasswordValidationPipe implements PipeTransform {
constructor(private passwordPolicyService: PasswordPolicyService) {}
transform(value: any, metadata: ArgumentMetadata) {
if (metadata.data === 'password') {
const validation = this.passwordPolicyService.validatePassword(value);
if (!validation.valid) {
throw new BadRequestException(validation.message);
}
}
return value;
}
}
Password Reset Flow
1. Generate Reset Token
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private passwordService: PasswordService,
private mailerService: MailerService,
) {}
async requestPasswordReset(email: string): Promise<void> {
const user = await this.usersService.findByEmail(email);
if (!user) {
return; // Don't reveal if email exists or not
}
const resetToken = randomBytes(32).toString('hex');
const resetExpires = new Date();
resetExpires.setHours(resetExpires.getHours() + 1); // 1 hour expiration
await this.usersService.setResetPasswordToken(
user.id,
resetToken,
resetExpires,
);
// Send email with reset link
await this.mailerService.sendPasswordResetEmail(
user.email,
resetToken,
);
}
}
2. Validate Reset Token
async validateResetToken(token: string): Promise<User | null> {
const user = await this.usersService.findByResetToken(token);
if (!user) {
return null;
}
// Check if token is expired
if (new Date() > user.resetPasswordExpires) {
return null;
}
return user;
}
3. Reset Password
async resetPassword(token: string, newPassword: string): Promise<boolean> {
const user = await this.validateResetToken(token);
if (!user) {
return false;
}
const hashedPassword = await this.passwordService.hash(newPassword);
await this.usersService.updatePassword(
user.id,
hashedPassword,
null, // Clear reset token
null, // Clear reset expiration
);
return true;
}
Password Change
async changePassword(
userId: string,
currentPassword: string,
newPassword: string,
): Promise<boolean> {
const user = await this.usersService.findById(userId);
if (!user) {
return false;
}
const isPasswordValid = await user.validatePassword(currentPassword);
if (!isPasswordValid) {
return false;
}
const hashedNewPassword = await this.passwordService.hash(newPassword);
await this.usersService.updatePassword(
userId,
hashedNewPassword,
null,
null,
);
return true;
}
Password History
For additional security, the starter kit can track password history to prevent reuse:
@Entity('password_history')
export class PasswordHistory {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => User)
user: User;
@Column()
password: string;
@CreateDateColumn()
createdAt: Date;
}
Preventing Password Reuse
async isPasswordReused(
userId: string,
newPassword: string,
): Promise<boolean> {
const history = await this.passwordHistoryRepository.find({
where: { user: { id: userId } },
order: { createdAt: 'DESC' },
take: 5, // Check against last 5 passwords
});
for (const entry of history) {
const isMatch = await this.passwordService.compare(
newPassword,
entry.password,
);
if (isMatch) {
return true;
}
}
return false;
}
Security Best Practices
- Never store plaintext passwords: Always hash passwords with bcrypt.
- Use strong salt values: The default salt rounds (12) provide a good balance of security and performance.
- Implement account lockout: Lock accounts after multiple failed login attempts.
- Avoid leaking information: Don't reveal if an email exists in password reset flows.
- Rate limit password attempts: Prevent brute force attacks by rate limiting login and reset attempts.
- Force password rotation: For sensitive applications, force password changes periodically.
- Check against common passwords: Prevent users from using well-known weak passwords.
- Use multi-factor authentication: For additional security, implement 2FA as described in the authentication section.