Skip to content

Commit

Permalink
Enhanced security
Browse files Browse the repository at this point in the history
Enhance security by repacking JWT with extra fields and signed by frontend
  • Loading branch information
dloh2236 committed Nov 9, 2024
1 parent 1d22cdd commit 0d89455
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 14 deletions.
93 changes: 86 additions & 7 deletions frontend/peerprep/auth/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
},
});

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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" };
}
Expand All @@ -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(
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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}`,
},
});

Expand Down Expand Up @@ -805,3 +861,26 @@ export const forgetPassword = async (identifier: string) => {
};
}
};

async function updateTokenWithField(token: string, field: string, secret: string): Promise<string> {
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");
}
}
28 changes: 25 additions & 3 deletions user-service/controller/user-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
}
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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` });
Expand Down
56 changes: 56 additions & 0 deletions user-service/middleware/basic-access-control.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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"];

Expand All @@ -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}`);
Expand Down
8 changes: 4 additions & 4 deletions user-service/routes/user-routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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)

Expand Down

0 comments on commit 0d89455

Please sign in to comment.