├── .gitignore ├── .DS_Store ├── reset-password.png ├── .env.example ├── readme.md ├── utils └── email │ ├── template │ ├── welcome.handlebars │ ├── resetPassword.handlebars │ └── requestResetPassword.handlebars │ └── sendEmail.js ├── routes └── index.route.js ├── models ├── Token.model.js └── User.model.js ├── db.js ├── index.js ├── controllers └── auth.controller.js ├── package.json └── services └── auth.service.js /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezesundayeze/forgotpassword/HEAD/.DS_Store -------------------------------------------------------------------------------- /reset-password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezesundayeze/forgotpassword/HEAD/reset-password.png -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | BCRYPT_SALT=10 2 | EMAIL_HOST= 3 | EMAIL_USERNAME= 4 | EMAIL_PORT= 5 | EMAIL_PASSWORD= 6 | FROM_EMAIL= 7 | DB_URL=mongodb://127.0.0.1:27017/testDB 8 | JWT_SECRET=mfefkuhio3k2rjkofn2mbikbkwjhnkj 9 | CLIENT_URL=localhost://8090 -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Secure Password Reset 2 | 3 | This is a password reset sample project 4 | 5 | Workflow 6 | 7 | ![Sample](reset-password.png "Workflow") 8 | 9 | Follow the tutorial [here]("") to learn the implementation 10 | 11 | 12 | -------------------------------------------------------------------------------- /utils/email/template/welcome.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 |

Hi {{name}},

9 |

Welcome to your new account

10 | 11 | -------------------------------------------------------------------------------- /utils/email/template/resetPassword.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 |

Hi {{name}},

9 |

Your password has been changed successfully

10 | 11 | -------------------------------------------------------------------------------- /utils/email/template/requestResetPassword.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 |

Hi {{name}},

9 |

You requested to reset your password.

10 |

Please, click the link below to reset your password

11 | Reset Password 12 | 13 | -------------------------------------------------------------------------------- /routes/index.route.js: -------------------------------------------------------------------------------- 1 | const { 2 | signUpController, 3 | resetPasswordRequestController, 4 | resetPasswordController, 5 | } = require("../controllers/auth.controller"); 6 | 7 | const router = require("express").Router(); 8 | 9 | router.post("/auth/signup", signUpController); 10 | router.post("/auth/requestResetPassword", resetPasswordRequestController); 11 | router.post("/auth/resetPassword", resetPasswordController); 12 | 13 | module.exports = router; 14 | -------------------------------------------------------------------------------- /models/Token.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const tokenSchema = new Schema({ 5 | userId: { 6 | type: Schema.Types.ObjectId, 7 | required: true, 8 | ref: "user", 9 | }, 10 | token: { 11 | type: String, 12 | required: true, 13 | }, 14 | createdAt: { 15 | type: Date, 16 | required: true, 17 | default: Date.now, 18 | expires: 900, 19 | }, 20 | }); 21 | 22 | module.exports = mongoose.model("Token", tokenSchema); 23 | -------------------------------------------------------------------------------- /db.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | let DB_URL = process.env.DB_URL; 3 | 4 | module.exports = async function connection() { 5 | try { 6 | await mongoose.connect( 7 | DB_URL, 8 | { 9 | useNewUrlParser: true, 10 | useUnifiedTopology: true, 11 | useFindAndModify: false, 12 | useCreateIndex: true, 13 | autoIndex: true, 14 | }, 15 | (error) => { 16 | if (error) return new Error("Failed to connect to database"); 17 | console.log("connected"); 18 | } 19 | ); 20 | } catch (error) { 21 | console.log(error); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require("express-async-errors"); 2 | require("dotenv").config(); 3 | 4 | const express = require("express"); 5 | const app = express(); 6 | const connection = require("./db"); 7 | const cors = require("cors"); 8 | const port = 8080; 9 | 10 | (async function db() { 11 | await connection(); 12 | })(); 13 | 14 | app.use(cors()); 15 | app.use(express.json()); 16 | 17 | // API routes 18 | app.use("/api/v1", require("./routes/index.route")); 19 | 20 | app.use((error, req, res, next) => { 21 | console.log(error) 22 | res.status(500).json({ error: error.message }); 23 | }); 24 | 25 | app.listen(port, () => { 26 | console.log("Listening to Port ", port); 27 | }); 28 | 29 | module.exports = app; 30 | -------------------------------------------------------------------------------- /models/User.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const bcrypt = require("bcrypt"); 3 | const Schema = mongoose.Schema; 4 | const bcryptSalt = process.env.BCRYPT_SALT; 5 | 6 | const userSchema = new Schema( 7 | { 8 | name: { 9 | type: String, 10 | trim: true, 11 | required: true, 12 | unique: true, 13 | }, 14 | email: { 15 | type: String, 16 | trim: true, 17 | unique: true, 18 | required: true, 19 | }, 20 | password: { type: String }, 21 | }, 22 | { 23 | timestamps: true, 24 | } 25 | ); 26 | 27 | userSchema.pre("save", async function (next) { 28 | if (!this.isModified("password")) { 29 | return next(); 30 | } 31 | const hash = await bcrypt.hash(this.password, Number(bcryptSalt)); 32 | this.password = hash; 33 | next(); 34 | }); 35 | 36 | module.exports = mongoose.model("user", userSchema); 37 | -------------------------------------------------------------------------------- /controllers/auth.controller.js: -------------------------------------------------------------------------------- 1 | const { 2 | signup, 3 | requestPasswordReset, 4 | resetPassword, 5 | } = require("../services/auth.service"); 6 | 7 | const signUpController = async (req, res, next) => { 8 | const signupService = await signup(req.body); 9 | return res.json(signupService); 10 | }; 11 | 12 | const resetPasswordRequestController = async (req, res, next) => { 13 | const requestPasswordResetService = await requestPasswordReset( 14 | req.body.email 15 | ); 16 | return res.json(requestPasswordResetService); 17 | }; 18 | 19 | const resetPasswordController = async (req, res, next) => { 20 | const resetPasswordService = await resetPassword( 21 | req.body.userId, 22 | req.body.token, 23 | req.body.password 24 | ); 25 | return res.json(resetPasswordService); 26 | }; 27 | 28 | module.exports = { 29 | signUpController, 30 | resetPasswordRequestController, 31 | resetPasswordController, 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "resetpassword", 3 | "version": "1.0.0", 4 | "description": "How to securely reset a user account passsword", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npm run test", 8 | "start": "nodemon start" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/ezesundayeze/forgotpassword.git" 13 | }, 14 | "keywords": [ 15 | "password", 16 | "forgot", 17 | "password" 18 | ], 19 | "author": "Eze Sunday Eze", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/ezesundayeze/forgotpassword/issues" 23 | }, 24 | "homepage": "https://github.com/ezesundayeze/forgotpassword#readme", 25 | "dependencies": { 26 | "bcrypt": "^5.0.0", 27 | "cors": "^2.8.5", 28 | "dotenv": "^8.2.0", 29 | "express": "^4.17.1", 30 | "express-async-errors": "^3.1.1", 31 | "handlebars": "^4.7.6", 32 | "jsonwebtoken": "^8.5.1", 33 | "mongoose": "^5.11.12", 34 | "nodemailer": "^6.4.17", 35 | "nodemon": "^2.0.7" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /utils/email/sendEmail.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require("nodemailer"); 2 | const handlebars = require("handlebars"); 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | 6 | const sendEmail = async (email, subject, payload, template) => { 7 | try { 8 | // create reusable transporter object using the default SMTP transport 9 | const transporter = nodemailer.createTransport({ 10 | host: process.env.EMAIL_HOST, 11 | port: 465, 12 | auth: { 13 | user: process.env.EMAIL_USERNAME, 14 | pass: process.env.EMAIL_PASSWORD, // naturally, replace both with your real credentials or an application-specific password 15 | }, 16 | }); 17 | 18 | const source = fs.readFileSync(path.join(__dirname, template), "utf8"); 19 | const compiledTemplate = handlebars.compile(source); 20 | const options = () => { 21 | return { 22 | from: process.env.FROM_EMAIL, 23 | to: email, 24 | subject: subject, 25 | html: compiledTemplate(payload), 26 | }; 27 | }; 28 | 29 | // Send email 30 | transporter.sendMail(options(), (error, info) => { 31 | if (error) { 32 | return error; 33 | } else { 34 | return res.status(200).json({ 35 | success: true, 36 | }); 37 | } 38 | }); 39 | } catch (error) { 40 | return error; 41 | } 42 | }; 43 | 44 | /* 45 | Example: 46 | sendEmail( 47 | "youremail@gmail.com, 48 | "Email subject", 49 | { name: "Eze" }, 50 | "./templates/layouts/main.handlebars" 51 | ); 52 | */ 53 | 54 | module.exports = sendEmail; 55 | -------------------------------------------------------------------------------- /services/auth.service.js: -------------------------------------------------------------------------------- 1 | const JWT = require("jsonwebtoken"); 2 | const User = require("../models/User.model"); 3 | const Token = require("../models/Token.model"); 4 | const sendEmail = require("../utils/email/sendEmail"); 5 | const crypto = require("crypto"); 6 | const bcrypt = require("bcrypt"); 7 | 8 | const JWTSecret = process.env.JWT_SECRET; 9 | const bcryptSalt = process.env.BCRYPT_SALT; 10 | const clientURL = process.env.CLIENT_URL; 11 | 12 | const signup = async (data) => { 13 | let user = await User.findOne({ email: data.email }); 14 | if (user) { 15 | throw new Error("Email already exist", 422); 16 | } 17 | user = new User(data); 18 | const token = JWT.sign({ id: user._id }, JWTSecret); 19 | await user.save(); 20 | 21 | return (data = { 22 | userId: user._id, 23 | email: user.email, 24 | name: user.name, 25 | token: token, 26 | }); 27 | }; 28 | 29 | const requestPasswordReset = async (email) => { 30 | const user = await User.findOne({ email }); 31 | if (!user) throw new Error("Email does not exist"); 32 | 33 | let token = await Token.findOne({ userId: user._id }); 34 | if (token) await token.deleteOne(); 35 | 36 | let resetToken = crypto.randomBytes(32).toString("hex"); 37 | const hash = await bcrypt.hash(resetToken, Number(bcryptSalt)); 38 | 39 | await new Token({ 40 | userId: user._id, 41 | token: hash, 42 | createdAt: Date.now(), 43 | }).save(); 44 | 45 | const link = `${clientURL}/passwordReset?token=${resetToken}&id=${user._id}`; 46 | 47 | sendEmail( 48 | user.email, 49 | "Password Reset Request", 50 | { 51 | name: user.name, 52 | link: link, 53 | }, 54 | "./template/requestResetPassword.handlebars" 55 | ); 56 | return { link }; 57 | }; 58 | 59 | const resetPassword = async (userId, token, password) => { 60 | let passwordResetToken = await Token.findOne({ userId }); 61 | 62 | if (!passwordResetToken) { 63 | throw new Error("Invalid or expired password reset token"); 64 | } 65 | 66 | console.log(passwordResetToken.token, token); 67 | 68 | const isValid = await bcrypt.compare(token, passwordResetToken.token); 69 | 70 | if (!isValid) { 71 | throw new Error("Invalid or expired password reset token"); 72 | } 73 | 74 | const hash = await bcrypt.hash(password, Number(bcryptSalt)); 75 | 76 | await User.updateOne( 77 | { _id: userId }, 78 | { $set: { password: hash } }, 79 | { new: true } 80 | ); 81 | 82 | const user = await User.findById({ _id: userId }); 83 | 84 | sendEmail( 85 | user.email, 86 | "Password Reset Successfully", 87 | { 88 | name: user.name, 89 | }, 90 | "./template/resetPassword.handlebars" 91 | ); 92 | 93 | await passwordResetToken.deleteOne(); 94 | 95 | return { message: "Password reset was successful" }; 96 | }; 97 | 98 | module.exports = { 99 | signup, 100 | requestPasswordReset, 101 | resetPassword, 102 | }; 103 | --------------------------------------------------------------------------------