diff --git a/frontend/peerprep/auth/actions.ts b/frontend/peerprep/auth/actions.ts index 5d77b9e328..783d3dc27b 100644 --- a/frontend/peerprep/auth/actions.ts +++ b/frontend/peerprep/auth/actions.ts @@ -5,7 +5,7 @@ import { TextEncoder } from "util"; import { getIronSession } from "iron-session"; import { env } from "next-runtime-env"; import { cookies } from "next/headers"; -import { jwtVerify } from "jose"; +import { jwtVerify, SignJWT } from "jose"; import { sessionOptions, @@ -365,10 +365,19 @@ export const verifyCode = async (code: number) => { } if (verificationCode === code) { + // Add the verified field to the payload + const updatedPayload = { ...payload, verified: true }; + + // Re-sign the token with the updated payload + const updatedToken = await new SignJWT(updatedPayload) + .setProtectedHeader({ alg: "HS256" }) + .setExpirationTime("1m") // Set the expiration time as needed + .sign(secretKey); + const response = await fetch(`${USER_SERVICE_URL}/auth/${payload.id}`, { method: "PATCH", headers: { - Authorization: `Bearer ${emailToken}`, + Authorization: `Bearer ${updatedToken}`, // Use the updated token }, }); @@ -447,15 +456,25 @@ export const editUsername = async (newUsername: string) => { } const session = await getSession(); + const secret = env("JWT_SECRET"); + + if (!session.accessToken) { + return { status: "error", message: "User session has expired" }; + } + + if (!secret) { + return { status: "error", message: "Internal application error" }; + } try { + const updatedToken = await updateTokenWithField(session.accessToken, "username", secret); const response = await fetch( `${USER_SERVICE_URL}/users/${session.userId}`, { method: "PATCH", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${session.accessToken}`, + Authorization: `Bearer ${updatedToken}`, }, body: JSON.stringify({ username: newUsername, @@ -547,6 +566,7 @@ export const editEmailRequest = async (newEmail: string) => { export const verifyEmailCode = async (code: number, newEmail: string) => { const session = await getSession(); + const accessToken = await getAccessToken(); const emailChangeSession = await getEmailChangeSession(); console.log(code, newEmail); @@ -562,6 +582,13 @@ export const verifyEmailCode = async (code: number, newEmail: string) => { }; } + if (!accessToken) { + return { + status: "error", + message: "User session has expired", + }; + } + if (!secret) { return { status: "error", message: "Internal application error" }; } @@ -583,6 +610,8 @@ export const verifyEmailCode = async (code: number, newEmail: string) => { } if (verificationCode === code) { + const updatedToken = await updateTokenWithField(accessToken, "email", secret); + // If code matches, update the email console.log(newEmail); const response = await fetch( @@ -591,7 +620,7 @@ export const verifyEmailCode = async (code: number, newEmail: string) => { method: "PATCH", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${session.accessToken}`, + Authorization: `Bearer ${updatedToken}`, // Use the updated token }, body: JSON.stringify({ email: newEmail, @@ -646,14 +675,26 @@ export const clearEmailChangeSession = async () => { export const changePassword = async (newPassword: string) => { const session = await getSession(); + const secret = env("JWT_SECRET"); + + if (!session.accessToken) { + return { status: "error", message: "User session has expired" }; + } + + if (!secret) { + return { status: "error", message: "Internal application error" }; + } + try { + const updatedToken = await updateTokenWithField(session.accessToken, "password", secret); + const response = await fetch( `${USER_SERVICE_URL}/users/${session.userId}`, { method: "PATCH", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${session.accessToken}`, + Authorization: `Bearer ${updatedToken}`, }, body: JSON.stringify({ password: newPassword, @@ -705,11 +746,13 @@ export const resetPassword = async (newPassword: string) => { const userId = payload.id; + const updatedToken = await updateTokenWithField(resetToken, "password", secret); + const response = await fetch(`${USER_SERVICE_URL}/users/${userId}`, { method: "PATCH", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${session.resetToken}`, + Authorization: `Bearer ${updatedToken}`, }, body: JSON.stringify({ password: newPassword, @@ -736,11 +779,24 @@ export const resetPassword = async (newPassword: string) => { export const deleteUser = async () => { const session = await getSession(); + const accessToken = await getAccessToken(); + + const secret = env("JWT_SECRET"); + + if (!session.accessToken) { + return { status: "error", message: "User session has expired" }; + } + + if (!secret) { + return { status: "error", message: "Internal application error" }; + } + + const updatedToken = await updateTokenWithField(session.accessToken, "delete", secret); const response = await fetch(`${USER_SERVICE_URL}/users/${session.userId}`, { method: "DELETE", headers: { - Authorization: `Bearer ${session.accessToken}`, + Authorization: `Bearer ${updatedToken}`, }, }); @@ -805,3 +861,26 @@ export const forgetPassword = async (identifier: string) => { }; } }; + +async function updateTokenWithField(token: string, field: string, secret: string): Promise { + const secretKey = new TextEncoder().encode(secret); + + try { + // Verify the token and extract the payload + const { payload } = await jwtVerify(token, secretKey); + + // Add the new field to the payload + const updatedPayload = { ...payload, field }; + + // Re-sign the token with the updated payload + const updatedToken = await new SignJWT(updatedPayload) + .setProtectedHeader({ alg: "HS256" }) + .setExpirationTime("1m") // Set the expiration time as needed + .sign(secretKey); + + return updatedToken; + } catch (error) { + console.error("Error updating token:", error); + throw new Error("Failed to update token"); + } +} diff --git a/user-service/controller/user-controller.js b/user-service/controller/user-controller.js index 2c258557f3..aa4be6d385 100644 --- a/user-service/controller/user-controller.js +++ b/user-service/controller/user-controller.js @@ -252,7 +252,7 @@ export async function updateUser(req, res) { console.log(`[USER] Update request - ID: ${userId}, New Username: ${username}, New Email: ${email}`); try { - if (!username && !email && !req.body.password) { + if (!username && !email && !password) { console.log(`[USER] Update failed - No fields to update - ID: ${userId}`); return res.status(400).json({ message: "No field to update" }); } @@ -286,11 +286,28 @@ export async function updateUser(req, res) { let hashedPassword; if (password) { if (!isValidPassword(password)) { + console.log(`[USER] Update failed - Password does not meet requirements`); return res.status(400).json({ message: "Password does not meet requirements" }); } - const salt = bcrypt.genSaltSync(10); - hashedPassword = bcrypt.hashSync(password, salt); + if (req.field === "password") { + const salt = bcrypt.genSaltSync(10); + hashedPassword = bcrypt.hashSync(password, salt); + } else { + console.log(`[USER] Update failed - Unauthorized password update ${userId}`); + return res.status(403).json({ message: "Unauthorized password update" }); + } + } + + if (username && req.field !== "username") { + console.log(`[USER] Update failed - Unauthorized username update - ID: ${userId}`); + return res.status(403).json({ message: "Unauthorized username update" }); + } + + if (email && req.field !== "email") { + console.log(`[USER] Update failed - Unauthorized email update - ID: ${userId}`); + return res.status(403).json({ message: "Unauthorized email update" }); } + const updatedUser = await _updateUserById(userId, username, email, hashedPassword); console.log(`[USER] User updated successfully - ID: ${userId}`); return res.status(200).json({ @@ -347,6 +364,11 @@ export async function deleteUser(req, res) { return res.status(404).json({ message: `User ${userId} not found` }); } + if (req.field !== "delete") { + console.log(`[USER] Delete failed - Unauthorized delete - ID: ${userId}`); + return res.status(403).json({ message: "Unauthorized delete" }); + } + await _deleteUserById(userId); console.log(`[USER] User deleted successfully - ID: ${userId}`); return res.status(200).json({ message: `Deleted user ${userId} successfully` }); diff --git a/user-service/middleware/basic-access-control.js b/user-service/middleware/basic-access-control.js index d0b745ec62..469588f532 100644 --- a/user-service/middleware/basic-access-control.js +++ b/user-service/middleware/basic-access-control.js @@ -21,6 +21,9 @@ export function verifyAccessToken(req, res, next) { if (!dbUser) { console.log(`[AUTH] Token verification failed: User not found - ID: ${user.id}`); return res.status(401).json({ message: "Authentication failed" }); + } else if (!dbUser.isVerified) { + console.log(`[AUTH] Token verification failed: Unverified account - ${dbUser.username}`); + return res.status(403).json({message: "You have not verified your account"}); } req.user = { @@ -38,6 +41,54 @@ export function verifyAccessToken(req, res, next) { }); } +export function verifyAccessTokenForUpdate(req, res, next) { + const authHeader = req.headers["authorization"]; + + if (!authHeader) { + console.log("[AUTH] Token verification failed: Missing authorization header"); + return res.status(401).json({ message: "Authentication failed" }); + } + + const token = authHeader.split(" ")[1]; + jwt.verify(token, process.env.JWT_SECRET, async (err, user) => { + if (err) { + console.log(`[AUTH] Token verification failed: Invalid token - ${err.message}`); + return res.status(401).json({ message: "Authentication failed" }); + } + + try { + const dbUser = await _findUserById(user.id); + if (!dbUser) { + console.log(`[AUTH] Token verification failed: User not found - ID: ${user.field}`); + return res.status(401).json({ message: "Authentication failed" }); + } else if (!dbUser.isVerified) { + console.log(`[AUTH] Token verification failed: Unverified account - ${dbUser.username}`); + return res.status(403).json({message: "You have not verified your account"}); + } + + if (!user.field) { + console.log(`[AUTH] User update details failed: Invalid JWT payload`); + return res.status(401).json({ message: "Token is invalid" }); + } else { + req.field = user.field; + } + + req.user = { + id: dbUser.id, + username: dbUser.username, + email: dbUser.email, + isAdmin: dbUser.isAdmin + }; + + console.log(`[AUTH] Token verified for user: ${dbUser.username} (${dbUser.id})`); + next(); + } catch (error) { + console.error(`[AUTH] Database error during token verification: ${error.message}`); + return res.status(500).json({ message: "Internal server error" }); + } + }); +} + export function verifyEmailToken(req, res, next) { const authHeader = req.headers["authorization"]; @@ -59,6 +110,11 @@ export function verifyEmailToken(req, res, next) { return res.status(401).json({ message: "Authentication failed" }); } + if (!user.verified) { + console.log(`[AUTH] Email token verification failed: Token not verified`); + return res.status(401).json({ message: "Code has not been verified" }); + } + const dbUser = await _findUserById(user.id); if (!dbUser) { console.log(`[AUTH] Email token verification failed: User not found - ID: ${user.id}`); diff --git a/user-service/routes/user-routes.js b/user-service/routes/user-routes.js index 8c16321593..1b0119e30e 100644 --- a/user-service/routes/user-routes.js +++ b/user-service/routes/user-routes.js @@ -17,7 +17,7 @@ import { updateEmailRequest, handleGetMatchHistory, } from "../controller/user-controller.js"; -import { verifyAccessToken, verifyEmailToken, verifyIsAdmin, verifyIsOwnerOrAdmin } from "../middleware/basic-access-control.js"; +import { verifyAccessToken, verifyAccessTokenForUpdate, verifyEmailToken, verifyIsAdmin, verifyIsOwnerOrAdmin } from "../middleware/basic-access-control.js"; const router = express.Router(); @@ -41,13 +41,13 @@ router.patch("/:id/resend-request", verifyEmailToken, refreshEmailToken); router.delete("/:email/request", deleteUserRequest); -router.get("/:id", verifyAccessToken, verifyIsOwnerOrAdmin, getUser); +router.get("/:id", verifyAccessTokenForUpdate, verifyIsOwnerOrAdmin, getUser); -router.patch("/:id", verifyAccessToken, verifyIsOwnerOrAdmin, updateUser); +router.patch("/:id", verifyAccessTokenForUpdate, verifyIsOwnerOrAdmin, updateUser); router.post("/:id/email-update-request", verifyAccessToken, verifyIsOwnerOrAdmin, updateEmailRequest); -router.delete("/:id", verifyAccessToken, verifyIsOwnerOrAdmin, deleteUser); +router.delete("/:id", verifyAccessTokenForUpdate, verifyIsOwnerOrAdmin, deleteUser); router.get("/:id/match-history", verifyAccessToken, handleGetMatchHistory)