password reset
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user