mysqlnode.jsauthenticationencryptionangular-cli

Password mismatch issue with bcryptjs in Angular, NodeJS, and MySQL


I've recently got myself in a problem while I was doing a personal project using Angular, NodeJS and MySQL. I was doing the sign up and log in processes and I decided to encrypt password using bycrptjs. The problem is that it is not working because when the sign up form is submitted it shows me an encrypted password but after I verified my account and went to the log in the problem came when I received my personal alert:

Password doesn't match! 4 attempts remaining!

I didn't understood why it came such response, because I inserted "Password@1234" for both sign up and log in as shown in the images below: Sign up page Above is the sign up page Log in page Above is the log in page I try to verify this by logging into the server console both passwords and it came like this:

Password stored in DB: $2a$12$.qMLUVnYfaLgQtJU9HWMxuBQea/W8AOGD2NuCHq5Q9OVLXXmBvtwG
Inputted password: $2a$12$UTCJpuu34rc9BiFbCQoHQuhfXpJVfrV5.zIVTfdK/7hPJ/Axygcg6

And then, when I tried to surf in Internet I realized that I cannot convert those encrypted passwords due to security purposes. Here's my Angular code:

Front End

sign-up.component.ts:

import { CommonModule } from '@angular/common';
import { HttpClient, HttpErrorResponse, HttpClientModule } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { AbstractControl, FormBuilder, FormGroup, ReactiveFormsModule, ValidationErrors, ValidatorFn, Validators } from '@angular/forms';

interface SignUpResponse {
  message: string;
  status: number
}

@Component({
  selector: 'app-sign-up',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule, HttpClientModule],
  templateUrl: './sign-up.component.html',
  styleUrl: './sign-up.component.css'
})
export class SignUpComponent implements OnInit {
  hide: boolean = true;
  signUpForm!: FormGroup;
  logInForm!: FormGroup;
  signUpSuccess: boolean = false;
  verifyPinForm: any;
  alertPlaceholder1!: HTMLDivElement;
  alertPlaceholder2!: HTMLDivElement;

  constructor(private httpClient: HttpClient, private fb: FormBuilder) {}

  ngOnInit(): void {
    this.signUpForm = this.fb.group({
      userName: ['', Validators.required],
      userEmail: ['', [Validators.required, Validators.email]],
      userContact: ['', Validators.required],
      userDOB: ['', [Validators.required, minimumAgeValidator(18)]],
      userPwd: ['', [Validators.required, Validators.minLength(8), this.passwordStrengthValidator()]],
      confirmPwd: ['', [Validators.required, Validators.minLength(8), this.passwordStrengthValidator()]]
    }, { validators: this.fieldsMatchValidator('userPwd', 'confirmPwd') });
    this.verifyPinForm = this.fb.group({
      pin: ['', Validators.required]
    });
    this.alertPlaceholder1 = document.getElementById('liveAlertPlaceholder1') as HTMLDivElement;
    this.alertPlaceholder2 = document.getElementById('liveAlertPlaceholder2') as HTMLDivElement;
  }

  passwordStrengthValidator(): ValidatorFn {
    return Validators.pattern('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[\\W_])[A-Za-z\\d\\W_].{8,}$');
  }

  fieldsMatchValidator(...fields: string[]): ValidatorFn {
    return (group: AbstractControl): { [key: string]: any } | null => {
      let isValid = true;
      for (let i = 0; i < fields.length; i += 2) {
        let field = group.get(fields[i]);
        let matchingField = group.get(fields[i + 1]);
        if (field && matchingField && field.value !== matchingField.value) {
          isValid = false;
          matchingField.setErrors({ fieldsDoNotMatch: true });
        } else {
          matchingField?.setErrors(null);
        }
      }
      return isValid ? null : { 'fieldsDoNotMatch': true };
    };
  }

  signUp(): void {
    if (this.signUpForm.valid) {
      const formData = this.signUpForm.value;
      this.httpClient.post('http://localhost:3000/sign-up', formData).subscribe(
        (response) => {
          const message = (response as SignUpResponse).message;
          this.appendAlert(message, "success", 1);
          this.signUpSuccess = true;
        },
        (error: HttpErrorResponse) => {
          this.appendAlert(error.message, "danger", 1)
        }
      );
    }
  }

  verifyPin(): void {
    const pin = this.verifyPinForm.get('pin')?.value;
    if (pin) {
      this.httpClient
        .post('http://localhost:3000/verify-pin', {
          userName: this.signUpForm.value.userName,
          verificationPin: pin,
        })
        .subscribe(
          (response: any) => {
            this.appendAlert(response.message, 'success', 2);
          },
          (error: HttpErrorResponse) => {
            this.appendAlert(error.error.message, 'danger', 2);
          }
        );
    }
  }

  appendAlert = (message: any, type: any, option: number): void => {
    const wrapper = document.createElement('div')
    if (type === 'success') {
      wrapper.innerHTML = [
        `<div class="alert alert-${type} alert-dismissible" role="alert">`,
        `   <div><i class="bi bi-check-circle-fill"></i> ${message}</div>`,
        '   <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
        '</div>'
      ].join('')
    } else {
      wrapper.innerHTML = [
        `<div class="alert alert-${type} alert-dismissible" role="alert">`,
        `   <div><i class="bi bi-x-circle-fill"></i> ${message}</div>`,
        '   <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
        '</div>'
      ].join('')
    }
    switch (option) {
      case 1:
        this.alertPlaceholder1.append(wrapper);
        break;
      case 2:
        this.alertPlaceholder2.append(wrapper);
        break;
      default:
        alert("ERROR! SOMETHING WENT WRONG!")
        break;
    }
  }
}

export function minimumAgeValidator(minAge: number) {
  return (control: AbstractControl): ValidationErrors | null => {
    const birthDate = new Date(control.value);
    const today = new Date();
    let age = today.getFullYear() - birthDate.getFullYear();
    const monthDifference = today.getMonth() - birthDate.getMonth();

    if (monthDifference < 0 || (monthDifference === 0 && today.getDate() < birthDate.getDate())) {
      age--;
    }

    return age >= minAge ? null : { 'minimumAge': { value: minAge } };
  };
}

log-in.component.ts:

import { CommonModule } from '@angular/common';
import { HttpClient, HttpErrorResponse, HttpClientModule } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, ValidatorFn, Validators } from '@angular/forms';

interface LogInResponse {
  message: string;
  status: number
}

@Component({
  selector: 'app-log-in',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule, HttpClientModule],
  templateUrl: './log-in.component.html',
  styleUrl: './log-in.component.css'
})
export class LogInComponent implements OnInit{
  logInForm!: FormGroup;
  hide: boolean = true;
  logInSuccess: boolean = false;
  alertPlaceholder!: HTMLDivElement;

  constructor(private httpClient: HttpClient, private fb: FormBuilder) { }

  ngOnInit(): void {
    this.logInForm = this.fb.group({
      userName: ['', Validators.required],
      userPwd: ['', [Validators.required, Validators.minLength(8), this.passwordStrengthValidator()]],
    });
    this.alertPlaceholder = document.getElementById('liveAlertPlaceholder3') as HTMLDivElement;
  }
  passwordStrengthValidator(): ValidatorFn {
    return Validators.pattern('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[\\W_])[A-Za-z\\d\\W_].{8,}$');
  }
  logIn(): void {
    if (this.logInForm.valid) {
      const formData = this.logInForm.value;
      this.httpClient.post('http://localhost:3000/log-in', formData).subscribe(
        (response) => {
          const message = (response as LogInResponse).message;
          this.appendAlert(message, "success", 1);
          this.logInSuccess = true;
        },
        (error: HttpErrorResponse) => {
          this.appendAlert(error.error.message, "danger", 1)
          if (error.error.message.includes('Account locked')) {
            this.logInForm.disable();
            setTimeout(() => this.logInForm.enable(), 300000); // 5 minutes
          }
        }
      );

    }
  }
  appendAlert = (message: any, type: any, option: number): void => {
    const wrapper = document.createElement('div')
    if (type === 'success') {
      wrapper.innerHTML = [
        `<div class="alert alert-${type} alert-dismissible" role="alert">`,
        `   <div><i class="bi bi-check-circle-fill"></i> ${message}</div>`,
        '   <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
        '</div>'
      ].join('')
    } else {
      wrapper.innerHTML = [
        `<div class="alert alert-${type} alert-dismissible" role="alert">`,
        `   <div><i class="bi bi-x-circle-fill"></i> ${message}</div>`,
        '   <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
        '</div>'
      ].join('')
    }
    switch (option) {
      case 1:
        this.alertPlaceholder.append(wrapper);
        break;
      default:
        alert("ERROR! SOMETHING WENT WRONG!")
        break;
    }

  }
}

Back End

sign-up.js:

"use strict"
const express = require('express');
const router = express.Router();
const connection = require('./db/connection');
const crypto = require('crypto');
const nodemailer = require('nodemailer');
const bcrypt = require('bcryptjs');

// Environment variables should be used here
const emailUser = process.env.EMAIL_USER;
const emailPass = process.env.EMAIL_PASS;

// Configure the email transport using the default SMTP transport and your email account details
const transporter = nodemailer.createTransport({
    service: 'gmail', // Use your email provider
    host: 'smtp.gmail.com',
    port: 465,
    secure: true,
    auth: {
        user: emailUser, // Your email address
        pass: emailPass // Your email password
    },
    tls: {
        rejectUnauthorized: true,
    }
});

//Encrypt password method
function encryptPassword (password) {
    const saltRounds = 12;
    const salt = bcrypt.genSaltSync(saltRounds);
    const hash = bcrypt.hashSync(password, salt);
    return hash;
}

// Sign-up endpoint
router.post('/sign-up', (req, res) => {
    const { userName, userEmail, userContact, userDOB, userPwd} = req.body;
    if (!userName || !userEmail || !userContact || !userDOB || !userPwd ) {
        return res.status(400).json({ error: 'All fields are required' });
    }

    const encryptedPwd = encryptPassword(userPwd);
    const verificationPin = crypto.randomInt(100000, 1000000).toString();
    const isVerified = 0;

    const sql = 'INSERT INTO users (name, email, contactNumber, dob, pwd, verificationPin, isVerified) VALUES (?, ?, ?, ?, ?, ?, ?)';
    const userValues = [userName, userEmail, userContact, userDOB, encryptedPwd, verificationPin, isVerified];

    connection.execute(sql, userValues, (err, _results) => {
        if (err) {
            console.error(err);
            return res.status(500).json({ error: 'Error while inserting data' });
        } else {
            sendEmail(userEmail, userName, verificationPin, (emailError) => {
                if (emailError) {
                    return res.status(500).json({ message: 'Something went wrong while sending email' });
                } else {
                    return res.status(200).json({ message: 'User has been created successfully! Now check your email to insert PIN below' });
                }
            });
        }
    });
});

// Verify PIN
router.post('/verify-pin', (req, res) => {
    const { userName ,verificationPin } = req.body;
    const sql = 'SELECT * FROM users WHERE name = ? AND verificationPin = ?';
    connection.query(sql, [userName, verificationPin], (err, results) => {
        console.log(sql, userName, verificationPin);
        console.log(results.length);
        if (err) {
            console.error('Error while verifying PIN:', err);
            return res.status(500).json({ error: 'Error while verifying PIN' });
        } else if (results.length > 0) {
            // PIN is correct, update the isVerified field for the user
            const updateSql = 'UPDATE users SET isVerified = 1 WHERE name = ?';
            connection.query(updateSql, [userName], (updateErr, updateResults) => {
                if (updateErr) {
                    console.error('Error while updating verification status:', updateErr);
                    return res.status(500).json({ error: 'Error while updating verification status' });
                } else {
                    console.log('User verified:', updateResults);
                    return res.status(200).json({ message: 'User has been verified successfully! Now you can log in.' });
                }
            });
        } else {
            // PIN is incorrect
            return res.status(401).json({ message: 'Incorrect PIN. Make sure you first copy and paste in the field from the email we sent you.' });
        }
    });
});

let sendEmail = (usrEmail, usrName, verifyPin, callback) => {
    const mailOptions = {
        from: emailUser,
        to: usrEmail,
        subject: 'Verify Your Account',
        text: `Hello ${usrName},\n\nWelcome to BeQuick!\n\nYour verification PIN is ${verifyPin}. Please enter this PIN to verify your account.\n\nThank you!\nBeQuick Company.`
    };

    transporter.sendMail(mailOptions, (error, info) => {
        if (error) {
            console.error('Error while sending email:', error);
            callback(error, null);
        } else {
            console.log('Email sent:', info.response);
            callback(null, info.response);
        }
    });
};

module.exports = router;

log-in.js:

"use strict";
const express = require('express');
const router = express.Router();
const connection = require('./db/connection');
const crypto = require('crypto');
const nodemailer = require('nodemailer');
const bcrypt = require('bcryptjs');

// Environment variables should be used here
const emailUser = process.env.EMAIL_USER;
const emailPass = process.env.EMAIL_PASS;

//Encrypt password method
function encryptPassword (password) {
    const saltRounds = 12;
    const salt = bcrypt.genSaltSync(saltRounds);
    const hash = bcrypt.hashSync(password, salt);
    return hash;
}

// Log-in endpoint
router.post('/log-in', (req, res) => {
    const { userName, userPwd } = req.body;
    const sql = 'SELECT * FROM users WHERE name = ?';

    connection.execute(sql, [userName], (err, results) => {
        if (err) {
            console.error('Error while logging: ', err);
            return res.status(500).json({ error: 'Oops! Something went wrong! Please try again later.' });
        } else {
            if (results.length === 0) {
                return res.status(400).json({ message: `Username doesn't exist! You may sign up or try again.` });
            } else {
                const user = results[0];
                const now = new Date();

                if (user.lock_until && now < new Date(user.lock_until)) {
                    return res.status(400).json({ message: `Account locked. Try again later. Account will be unlocked at ${user.lock_until}`});
                } else {
                    const encryptedPwd = encryptPassword(userPwd)
                    console.log("Password stored in DB: "+user.pwd);
                    console.log("Inputted password: "+encryptedPwd);
                    if (bcrypt.compareSync(encryptedPwd, user.pwd)) {
                        connection.execute('UPDATE users SET failed_attempts = 0, lock_until = NULL WHERE name = ?', [userName]);
                        return res.status(200).json({ message: 'You successfully logged in!' });
                    }
                     else {
                        let failedAttempts = user.failed_attempts + 1;
                        let lockUntil = null;

                        if (failedAttempts >= 5) {
                            lockUntil = new Date(now.getTime() + 5 * 60000); // 5 minutes lock
                        }

                        connection.execute('UPDATE users SET failed_attempts = ?, lock_until = ? WHERE name = ?', [failedAttempts, lockUntil, userName]);
                        return res.status(400).json({ message: `Password doesn't match! ${5 - failedAttempts} attempts remaining!` });
                    }
                }
            }
        }
    });
});

module.exports = router;

I am not understanding why does it behave like this. If you need anything else, please let me know in comments.

Any help would be appreciated. Thanks!


Solution

  • The Solution is too simple.

    No need to Encrypt the password while the login process, you can directly compare the req.body.password with the database stored password using the bcrypt.comapare() method.

    under the hood bcrypt encrypt the password with the same salt and compare with the encrypted password.

    I hope you understand the solution.