password reset

This commit is contained in:
jackiettran
2025-10-10 22:54:45 -04:00
parent 462dbf6b7a
commit b9e6cfc54d
15 changed files with 1976 additions and 178 deletions

View File

@@ -4,20 +4,30 @@ const { OAuth2Client } = require("google-auth-library");
const { User } = require("../models"); // Import from models/index.js to get models with associations
const logger = require("../utils/logger");
const emailService = require("../services/emailService");
const crypto = require("crypto");
const {
sanitizeInput,
validateRegistration,
validateLogin,
validateGoogleAuth,
validateForgotPassword,
validateResetPassword,
validateVerifyResetToken,
} = require("../middleware/validation");
const { csrfProtection, getCSRFToken } = require("../middleware/csrf");
const { loginLimiter, registerLimiter } = require("../middleware/rateLimiter");
const {
loginLimiter,
registerLimiter,
passwordResetLimiter,
} = require("../middleware/rateLimiter");
const router = express.Router();
const googleClient = new OAuth2Client(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
process.env.GOOGLE_REDIRECT_URI || "http://localhost:3000/auth/google/callback"
process.env.GOOGLE_REDIRECT_URI ||
"http://localhost:3000/auth/google/callback"
);
// Get CSRF token endpoint
@@ -76,17 +86,19 @@ router.post(
reqLogger.error("Failed to send verification email", {
error: emailError.message,
userId: user.id,
email: user.email
email: user.email,
});
// Continue with registration even if email fails
}
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, {
expiresIn: "15m", // Short-lived access token
});
const token = jwt.sign(
{ id: user.id, jwtVersion: user.jwtVersion },
process.env.JWT_SECRET,
{ expiresIn: "15m" } // Short-lived access token
);
const refreshToken = jwt.sign(
{ id: user.id, type: "refresh" },
{ id: user.id, jwtVersion: user.jwtVersion, type: "refresh" },
process.env.JWT_SECRET,
{ expiresIn: "7d" }
);
@@ -112,7 +124,7 @@ router.post(
reqLogger.info("User registration successful", {
userId: user.id,
username: user.username,
email: user.email
email: user.email,
});
res.status(201).json({
@@ -133,7 +145,7 @@ router.post(
error: error.message,
stack: error.stack,
email: req.body.email,
username: req.body.username
username: req.body.username,
});
res.status(500).json({ error: "Registration failed. Please try again." });
}
@@ -176,12 +188,14 @@ router.post(
// Reset login attempts on successful login
await user.resetLoginAttempts();
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, {
expiresIn: "15m", // Short-lived access token
});
const token = jwt.sign(
{ id: user.id, jwtVersion: user.jwtVersion },
process.env.JWT_SECRET,
{ expiresIn: "15m" } // Short-lived access token
);
const refreshToken = jwt.sign(
{ id: user.id, type: "refresh" },
{ id: user.id, jwtVersion: user.jwtVersion, type: "refresh" },
process.env.JWT_SECRET,
{ expiresIn: "7d" }
);
@@ -206,7 +220,7 @@ router.post(
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("User login successful", {
userId: user.id,
email: user.email
email: user.email,
});
res.json({
@@ -225,7 +239,7 @@ router.post(
reqLogger.error("Login error", {
error: error.message,
stack: error.stack,
email: req.body.email
email: req.body.email,
});
res.status(500).json({ error: "Login failed. Please try again." });
}
@@ -243,13 +257,17 @@ router.post(
const { code } = req.body;
if (!code) {
return res.status(400).json({ error: "Authorization code is required" });
return res
.status(400)
.json({ error: "Authorization code is required" });
}
// Exchange authorization code for tokens
const { tokens } = await googleClient.getToken({
code,
redirect_uri: process.env.GOOGLE_REDIRECT_URI || "http://localhost:3000/auth/google/callback",
redirect_uri:
process.env.GOOGLE_REDIRECT_URI ||
"http://localhost:3000/auth/google/callback",
});
// Verify the ID token from the token response
@@ -303,12 +321,14 @@ router.post(
}
// Generate JWT tokens
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, {
expiresIn: "15m",
});
const token = jwt.sign(
{ id: user.id, jwtVersion: user.jwtVersion },
process.env.JWT_SECRET,
{ expiresIn: "15m" }
);
const refreshToken = jwt.sign(
{ id: user.id, type: "refresh" },
{ id: user.id, jwtVersion: user.jwtVersion, type: "refresh" },
process.env.JWT_SECRET,
{ expiresIn: "7d" }
);
@@ -334,7 +354,9 @@ router.post(
reqLogger.info("Google authentication successful", {
userId: user.id,
email: user.email,
isNewUser: !user.createdAt || (Date.now() - new Date(user.createdAt).getTime()) < 1000
isNewUser:
!user.createdAt ||
Date.now() - new Date(user.createdAt).getTime() < 1000,
});
res.json({
@@ -351,9 +373,9 @@ router.post(
});
} catch (error) {
if (error.message && error.message.includes("invalid_grant")) {
return res
.status(401)
.json({ error: "Invalid or expired authorization code. Please try again." });
return res.status(401).json({
error: "Invalid or expired authorization code. Please try again.",
});
}
if (error.message && error.message.includes("redirect_uri_mismatch")) {
return res
@@ -364,7 +386,7 @@ router.post(
reqLogger.error("Google OAuth error", {
error: error.message,
stack: error.stack,
codePresent: !!req.body.code
codePresent: !!req.body.code,
});
res
.status(500)
@@ -546,10 +568,20 @@ router.post("/refresh", async (req, res) => {
return res.status(401).json({ error: "User not found" });
}
// Validate JWT version to invalidate old tokens after password change
if (decoded.jwtVersion !== user.jwtVersion) {
return res.status(401).json({
error: "Session expired due to password change. Please log in again.",
code: "JWT_VERSION_MISMATCH",
});
}
// Generate new access token
const newAccessToken = jwt.sign({ id: user.id }, process.env.JWT_SECRET, {
expiresIn: "15m",
});
const newAccessToken = jwt.sign(
{ id: user.id, jwtVersion: user.jwtVersion },
process.env.JWT_SECRET,
{ expiresIn: "15m" }
);
// Set new access token cookie
res.cookie("accessToken", newAccessToken, {
@@ -561,7 +593,7 @@ router.post("/refresh", async (req, res) => {
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Token refresh successful", {
userId: user.id
userId: user.id,
});
res.json({
@@ -579,7 +611,7 @@ router.post("/refresh", async (req, res) => {
reqLogger.error("Token refresh error", {
error: error.message,
stack: error.stack,
userId: req.user?.id
userId: req.user?.id,
});
res.status(401).json({ error: "Invalid or expired refresh token" });
}
@@ -589,7 +621,7 @@ router.post("/refresh", async (req, res) => {
router.post("/logout", (req, res) => {
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("User logout", {
userId: req.user?.id || 'anonymous'
userId: req.user?.id || "anonymous",
});
// Clear cookies
@@ -604,13 +636,221 @@ router.get("/status", optionalAuth, async (req, res) => {
if (req.user) {
res.json({
authenticated: true,
user: req.user
user: req.user,
});
} else {
res.json({
authenticated: false
authenticated: false,
});
}
});
// Forgot password endpoint
router.post(
"/forgot-password",
passwordResetLimiter,
csrfProtection,
sanitizeInput,
validateForgotPassword,
async (req, res) => {
try {
const { email } = req.body;
// Find user with local auth provider only
const user = await User.findOne({
where: {
email,
authProvider: "local",
},
});
// Always return success to prevent email enumeration
// Don't reveal whether the email exists or not
if (user) {
// Generate password reset token (returns plain token for email)
const resetToken = await user.generatePasswordResetToken();
// Send password reset email
try {
await emailService.sendPasswordResetEmail(user, resetToken);
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Password reset email sent", {
userId: user.id,
email: user.email,
});
} catch (emailError) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Failed to send password reset email", {
error: emailError.message,
userId: user.id,
email: user.email,
});
// Continue - don't reveal email sending failure to user
}
} else {
const reqLogger = logger.withRequestId(req.id);
reqLogger.info(
"Password reset requested for non-existent or OAuth user",
{
email: email,
}
);
}
// Always return success message (security best practice)
res.json({
message:
"If an account exists with that email, you will receive password reset instructions.",
});
} catch (error) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Forgot password error", {
error: error.message,
stack: error.stack,
email: req.body.email,
});
res.status(500).json({
error: "Failed to process password reset request. Please try again.",
});
}
}
);
// Verify reset token endpoint (optional - for frontend UX)
router.post(
"/verify-reset-token",
sanitizeInput,
validateVerifyResetToken,
async (req, res) => {
try {
const { token } = req.body;
// Hash the token to search for it in the database
const hashedToken = crypto
.createHash("sha256")
.update(token)
.digest("hex");
// Find user with this reset token (hashed)
const user = await User.findOne({
where: { passwordResetToken: hashedToken },
});
if (!user) {
return res.status(400).json({
valid: false,
error: "Invalid reset token",
code: "TOKEN_INVALID",
});
}
// Check if token is valid (not expired)
if (!user.isPasswordResetTokenValid(token)) {
return res.status(400).json({
valid: false,
error: "Reset token has expired. Please request a new one.",
code: "TOKEN_EXPIRED",
});
}
res.json({
valid: true,
});
} catch (error) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Verify reset token error", {
error: error.message,
stack: error.stack,
});
res.status(500).json({
valid: false,
error: "Failed to verify reset token. Please try again.",
});
}
}
);
// Reset password endpoint
router.post(
"/reset-password",
passwordResetLimiter,
csrfProtection,
sanitizeInput,
validateResetPassword,
async (req, res) => {
try {
const { token, newPassword } = req.body;
// Hash the token to search for it in the database
const crypto = require("crypto");
const hashedToken = crypto
.createHash("sha256")
.update(token)
.digest("hex");
// Find user with this reset token (hashed)
const user = await User.findOne({
where: { passwordResetToken: hashedToken },
});
if (!user) {
return res.status(400).json({
error: "Invalid or expired reset token",
code: "TOKEN_INVALID",
});
}
// Check if token is valid (not expired)
if (!user.isPasswordResetTokenValid(token)) {
return res.status(400).json({
error: "Reset token has expired. Please request a new one.",
code: "TOKEN_EXPIRED",
});
}
// Reset password (this will clear the token and hash the new password)
await user.resetPassword(newPassword);
// Send password changed notification email
try {
await emailService.sendPasswordChangedEmail(user);
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Password changed notification sent", {
userId: user.id,
email: user.email,
});
} catch (emailError) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Failed to send password changed notification", {
error: emailError.message,
userId: user.id,
email: user.email,
});
// Continue - don't fail password reset if email fails
}
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Password reset successful", {
userId: user.id,
email: user.email,
});
res.json({
message:
"Password has been reset successfully. You can now log in with your new password.",
});
} catch (error) {
const reqLogger = logger.withRequestId(req.id);
reqLogger.error("Reset password error", {
error: error.message,
stack: error.stack,
});
res.status(500).json({
error: "Failed to reset password. Please try again.",
});
}
}
);
module.exports = router;