Skip to content

Commit c9e4dd4

Browse files
authored
Merge pull request #55 from indrasuthar07/main
bug: Fix OAuth flow, session/token handling, and frontend OAuth navigation (Google & GitHub)
2 parents c52b33a + 689a6ec commit c9e4dd4

File tree

8 files changed

+342
-217
lines changed

8 files changed

+342
-217
lines changed

backend/src/controllers/authController.ts

Lines changed: 83 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +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 {
66
generateToken,
7-
generateAccessToken, // <-- RENAMED/UPDATED
8-
generateRefreshToken, // <-- ADDED
7+
generateRefreshToken,
98
} from "../utils/generateToken.js";
109
import { userSchema, loginSchema } from "../utils/validateInputs.js";
1110
import dotenv from "dotenv";
12-
1311
import { Session } from "../models/sessionModel.js";
1412

1513
dotenv.config();
14+
1615
const asTypedUser = (user: any): IUser & { _id: string } =>
1716
user as IUser & { _id: string };
1817

19-
// A helper function to send tokens
20-
const sendTokens = (res: Response, user: IUser & { _id: string }) => {
21-
const accessToken = generateAccessToken(user._id.toString());
22-
const newRefreshToken = generateRefreshToken(user._id.toString());
23-
24-
// Update user's refresh tokens in DB
25-
// Only one refresh token per user is supported (single device).
26-
user.refreshTokens = [newRefreshToken];
27-
28-
// Set refresh token in secure httpOnly cookie
29-
res.cookie("jwt", newRefreshToken, {
30-
httpOnly: true,
31-
secure: process.env.NODE_ENV !== "development",
32-
sameSite: "strict",
33-
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days (matches token expiry)
34-
});
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);
3530

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

44-
// ✅ SIGNUP CONTROLLER (Updated)
38+
//signup controller
4539
export const registerUser = async (
4640
req: Request,
4741
res: Response,
@@ -70,31 +64,29 @@ export const registerUser = async (
7064
const newUser = await User.create({ name, email, password: hashedPassword });
7165
const typedUser = asTypedUser(newUser);
7266

73-
const token = generateToken(typedUser._id.toString());
74-
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { exp?: number };
67+
// Generate tokens
68+
const accessToken = generateToken(typedUser._id.toString());
69+
const refreshToken = generateRefreshToken(typedUser._id.toString());
7570

76-
if (!decoded || !decoded.exp) {
77-
throw new Error("Invalid token format or missing expiration");
78-
}
71+
typedUser.refreshTokens = [refreshToken];
72+
await typedUser.save();
73+
74+
res.cookie("jwt", refreshToken, cookieOptions);
7975

80-
const expiresAt = new Date(decoded.exp * 1000);
81-
await Session.create({
82-
userId: typedUser._id,
83-
token,
84-
expiresAt,
85-
});
76+
//a session document
77+
await createSessionForAccessToken(typedUser._id.toString(), accessToken);
8678

8779
res.status(201).json({
8880
success: true,
8981
message: "User registered successfully",
90-
token,
82+
accessToken,
9183
});
9284
} catch (err) {
9385
next(err);
9486
}
9587
};
9688

97-
// ✅ LOGIN CONTROLLER (Updated)
89+
//login controller
9890
export const loginUser = async (
9991
req: Request,
10092
res: Response,
@@ -132,40 +124,31 @@ export const loginUser = async (
132124

133125
const typedUser = asTypedUser(foundUser);
134126

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

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

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

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

151-
res.json({
141+
return res.json({
152142
success: true,
153143
message: "Login successful",
154-
token,
144+
accessToken,
155145
});
156-
157-
// Redirect to the frontend without passing the token in the URL
158-
const redirectUrl = `${process.env.FRONTEND_URL}/auth-success`;
159-
res.redirect(redirectUrl);
160-
161146
} catch (err) {
162147
next(err);
163148
}
164149
};
165150

166-
// ... (keep all other functions as they are)
167-
168-
// ✅ REFRESH TOKEN CONTROLLER (Updated with fixes)
151+
//refresh token controller
169152
export const handleRefreshToken = async (
170153
req: Request,
171154
res: Response,
@@ -181,76 +164,64 @@ export const handleRefreshToken = async (
181164

182165
const refreshToken = cookies.jwt;
183166
// Clear the old cookie immediately
184-
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+
});
185172

186173
const foundUser = await User.findOne({ refreshTokens: refreshToken });
187174

188-
// Detected refresh token reuse!
189175
if (!foundUser) {
190176
try {
191177
const decoded = jwt.verify(
192178
refreshToken,
193179
process.env.JWT_REFRESH_SECRET as string
194180
) as { id: string };
195181

196-
// We know who the user is, now we hack-proof them
197-
// by deleting all their refresh tokens
198182
const compromisedUser = await User.findById(decoded.id);
199183
if (compromisedUser) {
200184
compromisedUser.refreshTokens = [];
201185
await compromisedUser.save();
202186
}
203187
} catch (err) {
204-
// Token was invalid in the first place
188+
// nothing to do
205189
} finally {
206190
return res
207191
.status(403)
208192
.json({ success: false, message: "Forbidden, token reuse" });
209193
}
210194
}
211195

212-
// Valid token, let's rotate it
213196
const typedUser = asTypedUser(foundUser);
214197

215198
try {
216-
// Verify the token is still valid
199+
// Verify
217200
jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET as string) as {
218201
id: string;
219202
};
220203

221204
// Generate new tokens
222-
const newAccessToken = generateAccessToken(typedUser._id.toString());
205+
const newAccessToken = generateToken(typedUser._id.toString());
223206
const newRefreshToken = generateRefreshToken(typedUser._id.toString());
224207

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

232-
// Assign the new array (filtered list + new token)
233211
typedUser.refreshTokens = [...otherRefreshTokens, newRefreshToken];
234212
await typedUser.save();
235213

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

244-
res.json({
220+
return res.json({
245221
success: true,
246222
accessToken: newAccessToken,
247223
});
248224
} catch (err) {
249-
// Token expired or invalid
250-
// ==================
251-
// FIX #2
252-
// ==================
253-
// Clear out the bad token, default to an empty array
254225
typedUser.refreshTokens =
255226
typedUser.refreshTokens?.filter((rt) => rt !== refreshToken) || [];
256227
await typedUser.save();
@@ -264,49 +235,47 @@ export const handleRefreshToken = async (
264235
}
265236
};
266237

267-
268-
// ✅ LOGOUT CONTROLLER (Updated with fix)
238+
//logout controller
269239
export const logoutUser = async (
270240
req: Request,
271241
res: Response,
272242
next: NextFunction
273243
) => {
274244
try {
275245
const cookies = req.cookies;
276-
if (!cookies?.jwt) {
277-
return res.sendStatus(204); // No cookie, already logged out
278-
}
279-
280-
const refreshToken = cookies.jwt;
281-
282-
// Find user and remove this specific refresh token
283-
const foundUser = await User.findOne({ refreshTokens: refreshToken });
284-
if (foundUser) {
285-
foundUser.refreshTokens =
286-
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+
}
287258

288-
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 });
289267
}
290268

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

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

311280
if (!user) {
312281
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)