Skip to content

Commit d9b8fa8

Browse files
committed
bug: fixing all authentication flow(google,github,manual)
1 parent 61f33d7 commit d9b8fa8

File tree

8 files changed

+346
-200
lines changed

8 files changed

+346
-200
lines changed

backend/src/controllers/authController.ts

Lines changed: 84 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,41 @@
11
import type { Request, Response, NextFunction } from "express";
22
import bcrypt from "bcryptjs";
3-
import jwt from "jsonwebtoken"; // <-- ADDED
3+
import jwt from "jsonwebtoken";
44
import User, { type IUser } from "../models/userModel.js";
55
import {
6-
generateAccessToken, // <-- RENAMED/UPDATED
7-
generateRefreshToken, // <-- ADDED
6+
generateToken,
7+
generateRefreshToken,
88
} from "../utils/generateToken.js";
99
import { userSchema, loginSchema } from "../utils/validateInputs.js";
1010
import dotenv from "dotenv";
11-
import jwt from "jsonwebtoken";
1211
import { Session } from "../models/sessionModel.js";
1312

1413
dotenv.config();
14+
1515
const asTypedUser = (user: any): IUser & { _id: string } =>
1616
user as IUser & { _id: string };
1717

18-
// A helper function to send tokens
19-
const sendTokens = (res: Response, user: IUser & { _id: string }) => {
20-
const accessToken = generateAccessToken(user._id.toString());
21-
const newRefreshToken = generateRefreshToken(user._id.toString());
22-
23-
// Update user's refresh tokens in DB
24-
// Only one refresh token per user is supported (single device).
25-
user.refreshTokens = [newRefreshToken];
26-
27-
// Set refresh token in secure httpOnly cookie
28-
res.cookie("jwt", newRefreshToken, {
29-
httpOnly: true,
30-
secure: process.env.NODE_ENV !== "development",
31-
sameSite: "strict",
32-
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days (matches token expiry)
33-
});
18+
const cookieOptions = {
19+
httpOnly: true,
20+
secure: process.env.NODE_ENV !== "development",
21+
sameSite: "strict" as const,
22+
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
23+
};
24+
25+
async function createSessionForAccessToken(userId: string, token: string) {
26+
const decoded = jwt.decode(token) as { exp?: number } | null;
27+
const expiresAt = decoded?.exp
28+
? new Date(decoded.exp * 1000)
29+
: new Date(Date.now() + 15 * 60 * 1000);
3430

35-
// Send access token in response body
36-
res.json({
37-
success: true,
38-
message: "Login successful",
39-
accessToken: accessToken,
31+
await Session.create({
32+
userId,
33+
token,
34+
expiresAt,
4035
});
41-
};
36+
}
4237

43-
// ✅ SIGNUP CONTROLLER (Updated)
38+
//signup controller
4439
export const registerUser = async (
4540
req: Request,
4641
res: Response,
@@ -69,31 +64,29 @@ export const registerUser = async (
6964
const newUser = await User.create({ name, email, password: hashedPassword });
7065
const typedUser = asTypedUser(newUser);
7166

72-
const token = generateToken(typedUser._id.toString());
73-
const decoded = jwt.decode(token) as { exp?: number } | null;
67+
// Generate tokens
68+
const accessToken = generateToken(typedUser._id.toString());
69+
const refreshToken = generateRefreshToken(typedUser._id.toString());
7470

75-
if (!decoded || !decoded.exp) {
76-
throw new Error("Invalid token format or missing expiration");
77-
}
71+
typedUser.refreshTokens = [refreshToken];
72+
await typedUser.save();
7873

79-
const expiresAt = new Date(decoded.exp * 1000);
80-
await Session.create({
81-
userId: typedUser._id,
82-
token,
83-
expiresAt,
84-
});
74+
res.cookie("jwt", refreshToken, cookieOptions);
75+
76+
//a session document
77+
await createSessionForAccessToken(typedUser._id.toString(), accessToken);
8578

8679
res.status(201).json({
8780
success: true,
8881
message: "User registered successfully",
89-
token,
82+
accessToken,
9083
});
9184
} catch (err) {
9285
next(err);
9386
}
9487
};
9588

96-
// ✅ LOGIN CONTROLLER (Updated)
89+
//login controller
9790
export const loginUser = async (
9891
req: Request,
9992
res: Response,
@@ -131,40 +124,31 @@ export const loginUser = async (
131124

132125
const typedUser = asTypedUser(foundUser);
133126

134-
const token = generateToken(typedUser._id.toString());
135-
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { exp?: number };
136-
137-
if (!decoded.exp) {
138-
throw new Error("Token missing expiration claim");
139-
}
127+
// Generate tokens
128+
const accessToken = generateToken(typedUser._id.toString());
129+
const refreshToken = generateRefreshToken(typedUser._id.toString());
140130

141-
const expiresAt = new Date(decoded.exp * 1000);
131+
// Store refresh token
132+
typedUser.refreshTokens = [refreshToken];
133+
await typedUser.save();
142134

143-
await Session.create({
144-
userId: typedUser._id,
145-
token,
146-
expiresAt,
147-
});
135+
// Set cookie
136+
res.cookie("jwt", refreshToken, cookieOptions);
148137

138+
// Create session
139+
await createSessionForAccessToken(typedUser._id.toString(), accessToken);
149140

150-
res.json({
141+
return res.json({
151142
success: true,
152143
message: "Login successful",
153-
token,
144+
accessToken,
154145
});
155-
156-
// Redirect to the frontend without passing the token in the URL
157-
const redirectUrl = `${process.env.FRONTEND_URL}/auth-success`;
158-
res.redirect(redirectUrl);
159-
160146
} catch (err) {
161147
next(err);
162148
}
163149
};
164150

165-
// ... (keep all other functions as they are)
166-
167-
// ✅ REFRESH TOKEN CONTROLLER (Updated with fixes)
151+
//refresh token controller
168152
export const handleRefreshToken = async (
169153
req: Request,
170154
res: Response,
@@ -180,76 +164,64 @@ export const handleRefreshToken = async (
180164

181165
const refreshToken = cookies.jwt;
182166
// Clear the old cookie immediately
183-
res.clearCookie("jwt", { httpOnly: true, sameSite: "strict", secure: process.env.NODE_ENV !== "development" });
167+
res.clearCookie("jwt", {
168+
httpOnly: true,
169+
sameSite: "strict",
170+
secure: process.env.NODE_ENV !== "development",
171+
});
184172

185173
const foundUser = await User.findOne({ refreshTokens: refreshToken });
186174

187-
// Detected refresh token reuse!
188175
if (!foundUser) {
189176
try {
190177
const decoded = jwt.verify(
191178
refreshToken,
192179
process.env.JWT_REFRESH_SECRET as string
193180
) as { id: string };
194181

195-
// We know who the user is, now we hack-proof them
196-
// by deleting all their refresh tokens
197182
const compromisedUser = await User.findById(decoded.id);
198183
if (compromisedUser) {
199184
compromisedUser.refreshTokens = [];
200185
await compromisedUser.save();
201186
}
202187
} catch (err) {
203-
// Token was invalid in the first place
188+
// nothing to do
204189
} finally {
205190
return res
206191
.status(403)
207192
.json({ success: false, message: "Forbidden, token reuse" });
208193
}
209194
}
210195

211-
// Valid token, let's rotate it
212196
const typedUser = asTypedUser(foundUser);
213197

214198
try {
215-
// Verify the token is still valid
199+
// Verify
216200
jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET as string) as {
217201
id: string;
218202
};
219203

220204
// Generate new tokens
221-
const newAccessToken = generateAccessToken(typedUser._id.toString());
205+
const newAccessToken = generateToken(typedUser._id.toString());
222206
const newRefreshToken = generateRefreshToken(typedUser._id.toString());
223207

224-
// ==================
225-
// FIX #1
226-
// ==================
227-
// Filter out the old token and default to an empty array
228208
const otherRefreshTokens =
229209
typedUser.refreshTokens?.filter((rt) => rt !== refreshToken) || [];
230210

231-
// Assign the new array (filtered list + new token)
232211
typedUser.refreshTokens = [...otherRefreshTokens, newRefreshToken];
233212
await typedUser.save();
234213

235-
// Send new tokens
236-
res.cookie("jwt", newRefreshToken, {
237-
httpOnly: true,
238-
secure: process.env.NODE_ENV !== "development",
239-
sameSite: "strict",
240-
maxAge: 7 * 24 * 60 * 60 * 1000,
241-
});
214+
//new refresh cookie
215+
res.cookie("jwt", newRefreshToken, cookieOptions);
216+
217+
//new session
218+
await createSessionForAccessToken(typedUser._id.toString(), newAccessToken);
242219

243-
res.json({
220+
return res.json({
244221
success: true,
245222
accessToken: newAccessToken,
246223
});
247224
} catch (err) {
248-
// Token expired or invalid
249-
// ==================
250-
// FIX #2
251-
// ==================
252-
// Clear out the bad token, default to an empty array
253225
typedUser.refreshTokens =
254226
typedUser.refreshTokens?.filter((rt) => rt !== refreshToken) || [];
255227
await typedUser.save();
@@ -263,49 +235,47 @@ export const handleRefreshToken = async (
263235
}
264236
};
265237

266-
267-
// ✅ LOGOUT CONTROLLER (Updated with fix)
238+
//logout controller
268239
export const logoutUser = async (
269240
req: Request,
270241
res: Response,
271242
next: NextFunction
272243
) => {
273244
try {
274245
const cookies = req.cookies;
275-
if (!cookies?.jwt) {
276-
return res.sendStatus(204); // No cookie, already logged out
277-
}
278-
279-
const refreshToken = cookies.jwt;
280-
281-
// Find user and remove this specific refresh token
282-
const foundUser = await User.findOne({ refreshTokens: refreshToken });
283-
if (foundUser) {
284-
foundUser.refreshTokens =
285-
foundUser.refreshTokens?.filter((rt) => rt !== refreshToken) || [];
246+
const authHeader = req.headers.authorization;
247+
const accessToken = authHeader?.split(" ")[1];
248+
249+
// If cookie exists- remove refresh token
250+
if (cookies?.jwt) {
251+
const refreshToken = cookies.jwt;
252+
const foundUser = await User.findOne({ refreshTokens: refreshToken });
253+
if (foundUser) {
254+
foundUser.refreshTokens =
255+
foundUser.refreshTokens?.filter((rt) => rt !== refreshToken) || [];
256+
await foundUser.save();
257+
}
286258

287-
await foundUser.save();
259+
res.clearCookie("jwt", {
260+
httpOnly: true,
261+
sameSite: "strict",
262+
secure: process.env.NODE_ENV !== "development",
263+
});
264+
}
265+
if (accessToken) {
266+
await Session.deleteOne({ token: accessToken });
288267
}
289268

290-
// Clear the cookie
291-
res.clearCookie("jwt", {
292-
httpOnly: true,
293-
sameSite: "strict",
294-
secure: process.env.NODE_ENV !== "development",
295-
});
296-
297-
res.status(200).json({ success: true, message: "Logged out successfully" });
269+
return res.status(200).json({ success: true, message: "Logged out successfully" });
298270
} catch (err) {
299271
next(err);
300272
}
301273
};
302274

303-
// ✅ GET PROFILE CONTROLLER (Unchanged, but for completeness)
275+
//get profile controller
304276
export const getUserProfile = async (req: Request, res: Response) => {
305277
try {
306-
// req.userId comes from the 'protect' middleware
307-
// @ts-ignore
308-
const user = await User.findById(req.userId).select("-password -refreshTokens");
278+
const user = await User.findById((req as any).userId).select("-password -refreshTokens");
309279

310280
if (!user) {
311281
return res.status(404).json({

backend/src/middleware/authMiddleware.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import type { Request, Response, NextFunction } from "express";
22
import jwt from "jsonwebtoken";
3-
import { Session } from "../models/sessionModel.js";
3+
import { Session } from "../models/sessionModel.js";
44

55
interface AuthRequest extends Request {
66
userId?: string;
77
}
88

9-
109
export const protect = async (req: AuthRequest, res: Response, next: NextFunction) => {
1110
try {
1211
const token = req.headers.authorization?.split(" ")[1];
@@ -17,10 +16,8 @@ export const protect = async (req: AuthRequest, res: Response, next: NextFunctio
1716
});
1817
}
1918

20-
21-
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { id: string };
19+
const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET as string) as { id: string };
2220

23-
2421
const activeSession = await Session.findOne({ token });
2522
if (!activeSession) {
2623
return res.status(401).json({
@@ -29,7 +26,7 @@ export const protect = async (req: AuthRequest, res: Response, next: NextFunctio
2926
});
3027
}
3128

32-
29+
// If expired then remove it
3330
if (activeSession.expiresAt < new Date()) {
3431
await Session.deleteOne({ token });
3532
return res.status(401).json({
@@ -46,4 +43,4 @@ export const protect = async (req: AuthRequest, res: Response, next: NextFunctio
4643
message: "Invalid or expired token",
4744
});
4845
}
49-
};
46+
};

0 commit comments

Comments
 (0)