Overview
- Add password reset functionality on top of a rest-api that has authentication and authorization implemented. If you want to know the basics of how jsonwebtoken is used for authorization, you can read here
- Prisma ORM is used with SQLite. So you don't have to setup any database in your system
- This approach is stateless and requires the least amount of db calls, therefore prioritizing performance
- The code is available on github
Flow
Table of Contents
Token Generation
utils/genToken.ts:
// helper function to generate token
export const genToken = (
user: any,
secret: string,
expiresIn: string,
tokenId?: string
) => {
const { id, username, email } = user
const jwtid = tokenId || randomUUID()
const token = jwt.sign({jwtid, id, username, email}, secret,{
algorithm: "HS256",
expiresIn,
})
return token
}
- Generate a token with a 2 minute expiration time (lesser the better)
- Use an environment variable as secret, this should be large and random
export const genPassResetToken = (user) => {
return genToken(user, process.env.PASS_RESET_TOKEN_SECRET!, "2m")
}
Why keep the validity duration small ?
As we are using stateless jwt, we can not invalidate the token. To prevent a user from using the same link again and again, we must set the expiration time to a lower value
Why not just use regular accessToken ?
A regular access token provides access to protected resources which we don't want at all.
The user only provides a email address, they may or may not be a legitimate user. Thats why we can't just hand over an accessToken on a password reset request
Sending Email
There are multiple ways to send email from a server.
Here is a sample code to setup a gmail account to send an email using nodemailer.
utils/sendEmail.ts:
export const sendEmail = async (receiverEmail, subject, body) => {
const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
// your gmail address
user: process.env.serviceEmail,
// app password for the gmail
pass: process.env.serviceEmailPassword,
},
})
const mailOptions = {
from: process.env.serviceEmail,
to: receiverEmail,
subject: subject,
html: body,
}
transporter.sendMail(mailOptions, (error, info) => {
if (error) {
console.log("Failed to send email to", receiverEmail)
} else {
/* console.log("Email sent: " + info.response) */
}
})
}
How to enable app password in google account
- Go to google account page
- Navigate to security and enable 2-step verification
- Search "App Passwords" in the search bar and click on the matched result
- Chose app as "Mail" and device as "Other" and put anything there
- Click on "GENERATE" button and save the password as
serviceEmailPassword
in .env file. (NOTE: you only get one chance to save the password)
Sending the reset link
controllers/passReset.ts:
// Route: GET /api/pass-reset; expecting email in request body
export const getPassResetLink = async (req, res) => {
try {
if (!req.body.email) {
return res.status(400).json({ msg: "No email provided" })
}
const user = await findUserByEmail(req.body.email)
if (!user || !user.email) return res.json({ msg: "Email not found" })
const token = genPassResetToken(user)
const requrl = req.protocol + "://" + req.get("host") + req.originalUrl
// the url from which the request came from, in local environment it is,
// http://localhost:5000/api/pass-reset
// if the frontend is in different domain declare PASS_RESET_URL in .env file
const url = process.env.PASS_RESET_URL || requrl
const resetLink = `<a target='_blank' href='${url}/${user.id}/${token}'>Password Reset Link</a>`
sendEmail(req.body.email, "Reset Password", resetLink)
res.json({ msg: "Password Reset Link sent to email" })
} catch (error) {
res.status(500).json({ msg: "Failed to send email" })
}
}
Resetting password
Password Reset Form
- The frontend should have a route "PASS_RESET_URL/:userId/:token" that takes an email in the body which sends a POST request to
/api/pass-reset/:userId/:token
on submit - Here, I have created the most basic html form to keep this article backend focused
utils/passReset.ts:
// Route : GET /api/pass-reset/:userId/:token
export const getPassResetPage = (req, res) => {
const { userId, token } = req.params
res.send(`<form action="/api/pass-reset/${userId}/${token}" method="POST">
<input type="password" name="password" value="" placeholder="Enter your new password..." />
<input type="submit" value="Reset Password" />
</form>`)
}
Updating password
Route : router.post("/api/pass-reset/:userId/:token", verifyPasswordResetToken, passReset)
- verifyPasswordResetToken middleware verifies the token with the secret PASS_RESET_TOKEN_SECRET
utils/passReset.ts:
export const passReset = async (req, res) => {
try {
const { password } = req.body
if (!password) return res.send("Password not provided")
updatePassword(req.params.userId, password)
res.json({ msg: "Password Reset Successfull" })
} catch (error) {
res.status(404).send("Failed to reset password")
}
}
- IMPORTANT: Hash the password before saving it in the database
- Ensure that you use the same hash function for password hashing in every cases such as user creation, change password, reset password
db/users.ts:
export const updatePassword = async (userId: string, newPassword: string) => {
newPassword = await hashString(newPassword)
return db.user.update({
where: { id: userId },
data: { password: newPassword },
})
}
Top comments (0)