Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ NODE_ENV=development

# Authentication
JWT_SECRET=your_jwt_secret
REFRESH_TOKEN_SECRET=your_refresh_token_secret

# Frontend URL
CLIENT_URL=http://localhost:5173
Expand Down
91 changes: 88 additions & 3 deletions backend/src/controllers/auth.controller.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { generateToken } from "../lib/utils.js";
import { generateToken, generateRefreshToken } from "../lib/utils.js";
import User from "../models/user.model.js";
import bcrypt from "bcryptjs";
import cloudinary from "../lib/cloudinary.js";
import { sendWelcomeEmail, sendOtpEmail, sendVerificationOtpEmail } from "../lib/sendEmail.js";
import crypto from "crypto";
import jwt from "jsonwebtoken";

//signup
// Get fullName, email, password from req.body
Expand Down Expand Up @@ -196,6 +197,11 @@
user.emailVerificationOtp = null;
user.emailVerificationOtpExpiry = null;

const token = generateToken(user._id);
const refreshToken = generateRefreshToken(user._id);
const refreshTokenHash = crypto.createHash("sha256").update(refreshToken).digest("hex");
user.refreshTokenHash = refreshTokenHash;

await user.save();

// Send welcome email asynchronously
Expand All @@ -213,7 +219,13 @@
}
});

const token = generateToken(user._id);
res.cookie("refreshToken", refreshToken, {
httpOnly: true,
sameSite: "strict",
secure: process.env.NODE_ENV === "production",
maxAge: 7 * 24 * 60 * 60 * 1000,
});

return res.status(200).json({
_id: user._id,
fullName: user.fullName,
Expand Down Expand Up @@ -302,6 +314,18 @@
}

const token = generateToken(user._id);
const refreshToken = generateRefreshToken(user._id);
const refreshTokenHash = crypto.createHash("sha256").update(refreshToken).digest("hex");

user.refreshTokenHash = refreshTokenHash;
await user.save();

res.cookie("refreshToken", refreshToken, {
httpOnly: true,
sameSite: "strict",
secure: process.env.NODE_ENV === "production",
maxAge: 7 * 24 * 60 * 60 * 1000,
});

res.status(200).json({
_id: user._id,
Expand All @@ -327,12 +351,24 @@
// Clear JWT cookie by setting empty value
// Set cookie expiration to 0
// Send success response
export const logout = (req, res) => {
export const logout = async (req, res) => {
try {
const refreshToken = req.cookies?.refreshToken;
if (refreshToken) {
const hash = crypto.createHash("sha256").update(refreshToken).digest("hex");
await User.findOneAndUpdate({ refreshTokenHash: hash }, { refreshTokenHash: null });
}

res.cookie("jwt", "", {
maxAge: 0,
});

res.clearCookie("refreshToken", {
httpOnly: true,
sameSite: "strict",
secure: process.env.NODE_ENV === "production",
});

res.status(200).json({
message: "Logged out successfully",
});
Expand All @@ -348,6 +384,55 @@
}
};

// refresh token
// Read refresh token from HTTP-only cookie
// Verify JWT
// Verify stored refresh token matches the user
// Generate and return a new access token
export const refresh = async (req, res) => {
try {
const refreshToken = req.cookies?.refreshToken;
if (!refreshToken) {
return res.status(400).json({
message: "Refresh token is missing",
});
}

let decoded;
try {
decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
} catch (err) {

Check warning on line 404 in backend/src/controllers/auth.controller.js

View workflow job for this annotation

GitHub Actions / backend-build

'err' is defined but never used
return res.status(401).json({
message: "Invalid or expired refresh token",
});
}

const hash = crypto.createHash("sha256").update(refreshToken).digest("hex");
const user = await User.findOne({
_id: decoded.userId,
refreshTokenHash: hash,
isBanned: false,
});

if (!user) {
return res.status(401).json({
message: "Invalid or expired refresh token",
});
}

const newAccessToken = generateToken(user._id);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return res.status(200).json({
token: newAccessToken,
});
} catch (error) {
console.error("Refresh error:", error.message);
return res.status(500).json({
message: "Internal Server Error",
});
}
};

//update profile
// Get profilePic from req.body
// Get authenticated user ID from req.user
Expand Down Expand Up @@ -502,7 +587,7 @@
message:
"Security questions saved",
});
} catch (error) {

Check warning on line 590 in backend/src/controllers/auth.controller.js

View workflow job for this annotation

GitHub Actions / backend-build

'error' is defined but never used
res.status(500).json({
message:
"Internal Server Error",
Expand Down Expand Up @@ -577,7 +662,7 @@
res.status(200).json({
resetToken,
});
} catch (error) {

Check warning on line 665 in backend/src/controllers/auth.controller.js

View workflow job for this annotation

GitHub Actions / backend-build

'error' is defined but never used
res.status(500).json({
message:
"Internal Server Error",
Expand Down Expand Up @@ -635,7 +720,7 @@
message:
"Password reset successful",
});
} catch (error) {

Check warning on line 723 in backend/src/controllers/auth.controller.js

View workflow job for this annotation

GitHub Actions / backend-build

'error' is defined but never used
res.status(500).json({
message:
"Internal Server Error",
Expand Down
10 changes: 10 additions & 0 deletions backend/src/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ export const generateToken = (userId) => {
}

return jwt.sign({ userId }, process.env.JWT_SECRET, {
expiresIn: "15m",
});
};

export const generateRefreshToken = (userId) => {
if (!process.env.REFRESH_TOKEN_SECRET) {
throw new Error("REFRESH_TOKEN_SECRET is not defined");
}

return jwt.sign({ userId }, process.env.REFRESH_TOKEN_SECRET, {
expiresIn: "7d",
});
};
11 changes: 9 additions & 2 deletions backend/src/middleware/auth.middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@ export const protectRoute = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;

if (!authHeader || !authHeader.startsWith("Bearer ")) {
if (!authHeader) {
return res.status(401).json({ message: "Unauthorized - No Token" });
}

const token = authHeader.split(" ")[1];
const trimmedHeader = authHeader.trim();
const parts = trimmedHeader.split(/\s+/);

if (parts.length < 2 || parts[0] !== "Bearer") {
return res.status(401).json({ message: "Unauthorized - No Token" });
}

const token = parts[1];

const decoded = jwt.verify(token, process.env.JWT_SECRET);

Expand Down
6 changes: 6 additions & 0 deletions backend/src/models/user.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ const userSchema = new mongoose.Schema(
type: Date,
default: null,
},

refreshTokenHash: {
type: String,
select: false,
default: null,
},
},
{ timestamps: true },
);
Expand Down
2 changes: 2 additions & 0 deletions backend/src/routes/auth.route.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getSecurityQuestions,
sendOtp,
verifyOtp,
refresh,
} from "../controllers/auth.controller.js";

import { protectRoute } from "../middleware/auth.middleware.js";
Expand All @@ -25,6 +26,7 @@ router.post("/verify-email", otpRateLimiter, verifyEmail);
router.post("/resend-verification-otp", otpRateLimiter, resendVerificationOtp);
router.post("/login", login);
router.post("/logout", logout);
router.post("/refresh", refresh);
router.get("/check", protectRoute, checkAuth);
router.put("/update-profile", protectRoute, updateProfile);
router.put("/security-questions", protectRoute, setupSecurityQuestions);
Expand Down
Loading
Loading