Start by creating a new Node.js project in any directory you prefer:
mkdir secure-password-reset
cd secure-password-reset
npm init -y
npm install express bcrypt jsonwebtoken dotenv body-parser
You'll want a structure like this:
secure-password-reset/
├── server/
│ ├── node_modules/
│ ├── config/
│ │ └── database.js
│ ├── routes/
│ │ └── auth.js
│ ├── controllers/
│ │ └── authController.js
│ ├── models/
│ │ └── user.js
│ ├── .env
│ ├── package.json
│ ├── app.js
Hook up your database in config/database.js
. For this, we’re going with MongoDB, so don't forget to install mongoose
:
npm install mongoose
And here’s config/database.js
:
const mongoose = require('mongoose');
require('dotenv').config();
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log('MongoDB connected');
} catch (err) {
console.error(err.message);
process.exit(1);
}
};
module.exports = connectDB;
Create a user model with email, password, and a reset token in models/user.js
.
models/user.js
:
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const UserSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
resetToken: String,
resetTokenExpiration: Date,
});
UserSchema.pre('save', async function (next) {
if (!this.isModified('password')) {
return next();
}
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
module.exports = mongoose.model('User', UserSchema);
Set up your routes in routes/auth.js
.
routes/auth.js
:
const express = require('express');
const { register, login, requestPasswordReset, resetPassword } = require('../controllers/authController');
const router = express.Router();
router.post('/register', register);
router.post('/login', login);
router.post('/request-password-reset', requestPasswordReset);
router.post('/reset-password', resetPassword);
module.exports = router;
The controller with registration and password reset logic.
controllers/authController.js
:
const User = require('../models/user');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const nodemailer = require('nodemailer');
require('dotenv').config();
const transporter = nodemailer.createTransport({
service: 'Gmail',
auth: {
user: process.env.EMAIL,
pass: process.env.EMAIL_PASSWORD,
},
});
exports.register = async (req, res) => {
const { email, password } = req.body;
try {
let user = await User.findOne({ email });
if (user) {
return res.status(400).json({ msg: 'User already exists' });
}
user = new User({ email, password });
await user.save();
res.status(201).json({ msg: 'User registered' });
} catch (err) {
console.error(err.message);
res.status(500).send('Server error');
}
};
exports.login = async (req, res) => {
const { email, password } = req.body;
try {
let user = await User.findOne({ email });
if (!user) {
return res.status(400).json({ msg: 'Invalid credentials' });
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(400).json({ msg: 'Invalid credentials' });
}
const payload = { user: { id: user.id } };
jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '1h' }, (err, token) => {
if (err) throw err;
res.json({ token });
});
} catch (err) {
console.error(err.message);
res.status(500).send('Server error');
}
};
exports.requestPasswordReset = async (req, res) => {
const { email } = req.body;
try {
let user = await User.findOne({ email });
if (!user) {
return res.status(400).json({ msg: 'User not found' });
}
const resetToken = crypto.randomBytes(32).toString('hex');
user.resetToken = resetToken;
user.resetTokenExpiration = Date.now() + 3600000; // 1 hour
await user.save();
const resetUrl = `http://localhost:3000/reset-password/${resetToken}`;
transporter.sendMail({
to: user.email,
from: 'noreply@test.com',
subject: 'Password Reset',
html: `<p>You requested a password reset</p><p>Click this <a href="${resetUrl}">link</a> to set a new password.</p>`
});
res.status(200).json({ msg: 'Reset link sent to email' });
} catch (err) {
console.error(err.message);
res.status(500).send('Server error');
}
};
exports.resetPassword = async (req, res) => {
const { token, newPassword } = req.body;
try {
let user = await User.findOne({ resetToken: token, resetTokenExpiration: { $gt: Date.now() } });
if (!user) {
return res.status(400).json({ msg: 'Invalid or expired token' });
}
user.password = newPassword;
user.resetToken = undefined;
user.resetTokenExpiration = undefined;
await user.save();
res.status(200).json({ msg: 'Password reset successfully' });
} catch (err) {
console.error(err.message);
res.status(500).send('Server error');
}
};
Set up Express and include middleware to parse request bodies.
app.js
:
const express = require('express');
const connectDB = require('./config/database');
const authRoutes = require('./routes/auth');
const bodyParser = require('body-parser');
require('dotenv').config();
const app = express();
// Connect to database
connectDB();
// Middleware
app.use(bodyParser.json());
// Define routes
app.use('/api/auth', authRoutes);
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server started on port ${PORT}`));
Don’t forget the .env
file in your server directory, which holds environment-specific settings:
.env
:
MONGO_URI=mongodb://localhost:27017/secure-password-reset
JWT_SECRET=your_jwt_secret_key
EMAIL=your_email@gmail.com
EMAIL_PASSWORD=your_email_password
This configuration gives you a robust password reset mechanism. Key security measures:
There's always room for improvement, especially in production. Think about rate limiting, sophisticated validation, account lockouts, and advanced monitoring of security practices to make the platform even more secure.