There are lots of tutorials about user authentication but all of them are just covering the topics like how to sign up and how to sign in. Even if you find a tutorial about reset password then you will see that it covers old school method without APIs and using jQuery based front-end. In this tutorial, we are going to use a Node.js based API which will connect to Angular front-end. Let’s get started:
npm init
So I assume you are already familiar with generating projects with Node.js and Angular or React. So will not go further for basic topics. We need to install the Express framework, Body-parser, and Mongoose and other required dependencies (We are using Mongo DB) and Node mailer for sending the link to your email where you can set a new password.
npm install express body-parser nodemailer mongoose bcrypt cors cookie-parser
Let’s create our main server file:
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const auth = require('./routes/authRoutes');
const app = express();
app.use(cors());
const server = require('http').createServer(app);
const dbConfig = require('./config/secret');
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
app.use(cookieParser());
mongoose.Promise = global.Promise;
mongoose.connect(
dbConfig.url,
{
useNewUrlParser: true
}
).then((conn) => {
console.log("Mongo Connected")
}).catch((err) => {
console.log(err)
})
app.use('/api/resetpassword', auth);
server.listen(3000, () => {
console.log('Listening on port 3000');
});
You may notice that we added a Mongo DB URL from the config folder. You can use Mongo DB Atlas and insert URL by yourself.
You might also notice
authroute
which is not created yet. We will do it later. However, before it we are going to create
User model
const bcrypt = require('bcryptjs');
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const userSchema = new Schema({
username: {
type: String,
min: [5, 'Too short, min is 5 characters'],
max: [32, 'Too long, max is 32 characters']
},
email: {
type: String,
min: [5, 'Too short, min is 5 characters'],
max: [32, 'Too long, max is 32 characters'],
unique: true,
lowercase: true,
required: 'Email is required',
match: [/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/]
},
password: {
type: String,
min: [5, 'Too short, min is 5 characters'],
max: [32, 'Too long, max is 32 characters'],
required: 'Password is required'
},
});
userSchema.statics.EncryptPassword = async function(password) { const hash = await bcrypt.hash(password, 12); return hash;};
module.exports = mongoose.model('User', userSchema);
We are using bcrypt for hashing our user's password.
Also, we need a create a model for reset token. Here is how it will look like.
const mongoose = require('mongoose');
const resettokenSchema = new mongoose.Schema({
_userId: { type: mongoose.Schema.Types.ObjectId, required: true, ref: 'User' },
resettoken: { type: String, required: true },
createdAt: { type: Date, required: true, default: Date.now, expires: 43200 },
});
module.exports = mongoose.model('passwordResetToken', resettokenSchema);
It’s time to create an auth controller that will contain all controller functions.
const bcrypt = require('bcryptjs');
const crypto = require('crypto');
const nodemailer = require('nodemailer');
const User = require('../models/users');
const passwordResetToken = require('../models/resettoken');
module.exports = {
async CreateUser(req, res) {
const userEmail = await User.findOne({
email: req.body.email
});
if (userEmail) {
return res
.status(409)
.json({ message: 'Email already exist' });
}
const userName = await User.findOne({
username: req.body.username
});
if (userName) {
return res
.status(409)
.json({ message: 'Username already exist' });
}
return bcrypt.hash(req.body.password, 10, (err, hash) => {
if (err) {
return res
.status(400)
.json({ message: 'Error hashing password' });
}
const body = {
username: req.body.username,
email: req.body.email,
password: hash
};
User.create(body)
.then(user => {
res
res.status(201) .json({ message: 'User created successfully', user });
})
.catch(() => {
res
.status(500)
.json({ message: 'Error occured' });
});
});
},
For making reset password work we first need to create an account. In the above, you will see the controller function which can be used for registering a user. We use
Bcrypt
for hashing a password. it's also recommended to use Joi
or express-validator
for adding extra validation. You might think why both bcrypt
and crypto
were required. Later we will use crypto
for resettoken
There is a debate which one of those libraries are better and which one can be selected in different situations. The main point of using both of them in this tutorial was that you can see in which cases each of them can be used and how?
async ResetPassword(req, res) {
if (!req.body.email) {
return res
.status(500)
.json({ message: 'Email is required' });
}
const user = await User.findOne({
email:req.body.email
});
if (!user) {
return res
.status(409)
.json({ message: 'Email does not exist' });
}
var resettoken = new passwordResetToken({ _userId: user._id, resettoken: crypto.randomBytes(16).toString('hex') });
resettoken.save(function (err) {
if (err) { return res.status(500).send({ msg: err.message }); }
passwordResetToken.find({ _userId: user._id, resettoken: { $ne: resettoken.resettoken } }).remove().exec();
res.status(200).json({ message: 'Reset Password successfully.' });
var transporter = nodemailer.createTransport({
service: 'Gmail',
port: 465,
auth: {
user: 'user',
pass: 'password'
}
});
var mailOptions = {
to: user.email,
from: 'your email',
subject: 'Node.js Password Reset',
text: 'You are receiving this because you (or someone else) have requested the reset of the password for your account.\n\n' +
'Please click on the following link, or paste this into your browser to complete the process:\n\n' +
'http://localhost:4200/response-reset-password/' + resettoken.resettoken + '\n\n' +
'If you did not request this, please ignore this email and your password will remain unchanged.\n'
}
transporter.sendMail(mailOptions, (err, info) => {
})
})
},
Above
ResetPassword
controller function generates a token which consists of random bytes and attaches it into the URL which will be included in the email. We use Node mailer to send email to the user and as a service provider Gmail was selected for this tutorial, but you can use Sendgrid's API. You can include from which email to send this email, customize the text and include your project’s domain name as the main URL. Localhost:4200 was added because Angular is running locally on that port. However this functionality works a bit slow and not enough well on the localhost. Wile it will behave perfectly in production mode when you will deploy it and connect with your domain name.Here is the final controller function which will be used for submitting new password.
async ValidPasswordToken(req, res) {
if (!req.body.resettoken) {
return res
.status(500)
.json({ message: 'Token is required' });
}
const user = await passwordResetToken.findOne({
resettoken: req.body.resettoken
});
if (!user) {
return res
.status(409)
.json({ message: 'Invalid URL' });
}
User.findOneAndUpdate({ _id: user._userId }).then(() => {
res.status(200).json({ message: 'Token verified successfully.' });
}).catch((err) => {
return res.status(500).send({ msg: err.message });
});
},
async NewPassword(req, res) {
passwordResetToken.findOne({ resettoken: req.body.resettoken }, function (err, userToken, next) {
if (!userToken) {
return res
.status(409)
.json({ message: 'Token has expired' });
}
User.findOne({
_id: userToken._userId
}, function (err, userEmail, next) {
if (!userEmail) {
return res
.status(409)
.json({ message: 'User does not exist' });
}
return bcrypt.hash(req.body.newPassword, 10, (err, hash) => {
if (err) {
return res
.status(400)
.json({ message: 'Error hashing password' });
}
userEmail.password = hash;
userEmail.save(function (err) {
if (err) {
return res
.status(400)
.json({ message: 'Password can not reset.' });
} else {
userToken.remove();
return res
.status(201)
.json({ message: 'Password reset successfully' });
}
});
});
});
})
}
}
Before submiting the new password it verifies the token with user ID.
Before completing the back-end we need to create
authrouter
file that will include all endpoints.
const express = require('express');
const router = express.Router();
const AuthCtrl = require('../controllers/auth');
router.post('/register', AuthCtrl.CreateUser);
router.post('/req-reset-password', AuthCtrl.ResetPassword);
router.post('/new-password', AuthCtrl.NewPassword);
router.post('/valid-password-token', AuthCtrl.ValidPasswordToken);
module.exports = router;
That’s it! The back-end is ready and completely functional. We need to care about the front end now. To check the second part of this tutorial please visit by this link