feat: another security sweep
Node.js CI / CI (push) Failing after 10s
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m50s

This commit is contained in:
2026-02-04 22:02:24 -08:00
parent 5eae636f2f
commit 9caf74945a
10 changed files with 416 additions and 36 deletions
+85 -5
View File
@@ -79,8 +79,9 @@ const authRoutes: FastifyPluginAsync = async (app) => {
// Create or update user in database
const user = await authService.createOrUpdateUserFromDiscord(userData, inDiscord, isVip, isMod, isStaff);
// Generate JWT
const jwt = await authService.generateToken(user);
// Generate access token and refresh token
const accessToken = await authService.generateToken(user);
const refreshToken = await authService.generateRefreshToken(user.id);
// Log successful login
await AuditService.log({
@@ -91,13 +92,21 @@ const authRoutes: FastifyPluginAsync = async (app) => {
success: true,
}, request);
// Set signed cookie and redirect to frontend
// Set signed cookies and redirect to frontend
reply
.setCookie("auth-token", jwt, {
.setCookie("auth-token", accessToken, {
path: "/",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 15 * 60, // 15 minutes
signed: true,
})
.setCookie("refresh-token", refreshToken, {
path: "/api/auth",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 7 * 24 * 60 * 60, // 7 days
signed: true,
})
@@ -143,11 +152,78 @@ const authRoutes: FastifyPluginAsync = async (app) => {
}
);
/**
* Refresh access token using refresh token.
*/
app.post("/refresh", async (request, reply) => {
const signedRefreshToken = request.cookies["refresh-token"];
if (!signedRefreshToken) {
return reply.code(401).send({ error: "No refresh token provided" });
}
const unsignedResult = request.unsignCookie(signedRefreshToken);
if (!unsignedResult.valid || !unsignedResult.value) {
return reply.code(401).send({ error: "Invalid refresh token" });
}
const refreshToken = unsignedResult.value;
const user = await authService.validateRefreshToken(refreshToken);
if (!user) {
reply.clearCookie("refresh-token", { path: "/api/auth", signed: true });
return reply.code(401).send({ error: "Refresh token expired or invalid" });
}
if (user.isBanned) {
await authService.revokeAllUserTokens(user.id);
reply
.clearCookie("auth-token", { path: "/", signed: true })
.clearCookie("refresh-token", { path: "/api/auth", signed: true });
return reply.code(403).send({ error: "Account is banned" });
}
// Rotate refresh token for security
const newRefreshToken = await authService.rotateRefreshToken(refreshToken, user.id);
if (!newRefreshToken) {
return reply.code(401).send({ error: "Failed to rotate refresh token" });
}
const newAccessToken = await authService.generateToken(user);
reply
.setCookie("auth-token", newAccessToken, {
path: "/",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 15 * 60, // 15 minutes
signed: true,
})
.setCookie("refresh-token", newRefreshToken, {
path: "/api/auth",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 7 * 24 * 60 * 60, // 7 days
signed: true,
})
.send({ user, accessToken: newAccessToken });
});
/**
* Logout.
*/
app.post("/logout", async (request, reply) => {
// Try to get user ID from JWT if available
// Revoke refresh token if present
const signedRefreshToken = request.cookies["refresh-token"];
if (signedRefreshToken) {
const unsignedResult = request.unsignCookie(signedRefreshToken);
if (unsignedResult.valid && unsignedResult.value) {
await authService.revokeRefreshToken(unsignedResult.value);
}
}
// Try to get user ID from JWT for audit logging
try {
await request.jwtVerify();
const user = request.user as { id?: string; username?: string };
@@ -169,6 +245,10 @@ const authRoutes: FastifyPluginAsync = async (app) => {
path: "/",
signed: true,
})
.clearCookie("refresh-token", {
path: "/api/auth",
signed: true,
})
.send({ message: "Logged out successfully" });
});