feat: Mejorar manejo de errores en el flujo de autenticación de Discord y asegurar el estado de OAuth

This commit is contained in:
Shni
2025-10-14 22:56:38 -05:00
parent 08dadae375
commit e46e8420b5

View File

@@ -707,9 +707,19 @@ export const server = createServer(
const qs = Object.fromEntries(url.searchParams.entries()); const qs = Object.fromEntries(url.searchParams.entries());
const { code, state } = qs as any; const { code, state } = qs as any;
// Validate state // Validate state
if (!state || !hasState(state)) { if (!state) {
res.writeHead(400, applySecurityHeadersForRequest(req)); res.writeHead(400, applySecurityHeadersForRequest(req));
return res.end("Invalid OAuth state"); return res.end("Missing OAuth state parameter");
}
if (!hasState(state)) {
console.warn("OAuth callback with invalid/expired state", {
state,
ip: clientIp,
});
res.writeHead(400, applySecurityHeadersForRequest(req));
return res.end(
"Invalid or expired OAuth state. Please try logging in again."
);
} }
const clientId = process.env.DISCORD_CLIENT_ID || ""; const clientId = process.env.DISCORD_CLIENT_ID || "";
const clientSecret = process.env.DISCORD_CLIENT_SECRET || ""; const clientSecret = process.env.DISCORD_CLIENT_SECRET || "";
@@ -719,7 +729,7 @@ export const server = createServer(
} }
const redirectUri = const redirectUri =
process.env.DISCORD_REDIRECT_URI || process.env.DISCORD_REDIRECT_URI ||
`http://${req.headers.host}/auth/callback`; `https://${req.headers.host}/auth/callback`;
if (!code) { if (!code) {
res.writeHead(400, applySecurityHeadersForRequest(req)); res.writeHead(400, applySecurityHeadersForRequest(req));
@@ -739,7 +749,12 @@ export const server = createServer(
redirect_uri: redirectUri, redirect_uri: redirectUri,
} as any).toString(), } as any).toString(),
}); });
if (!tokenRes.ok) throw new Error("Token exchange failed"); if (!tokenRes.ok) {
const text = await tokenRes.text().catch(() => "<no-body>");
throw new Error(
`Token exchange failed: ${tokenRes.status} ${tokenRes.statusText} ${text}`
);
}
const tokenJson = await tokenRes.json(); const tokenJson = await tokenRes.json();
const accessToken = tokenJson.access_token; const accessToken = tokenJson.access_token;
@@ -747,7 +762,12 @@ export const server = createServer(
const userRes = await fetch("https://discord.com/api/users/@me", { const userRes = await fetch("https://discord.com/api/users/@me", {
headers: { Authorization: `Bearer ${accessToken}` }, headers: { Authorization: `Bearer ${accessToken}` },
}); });
if (!userRes.ok) throw new Error("Failed fetching user"); if (!userRes.ok) {
const text = await userRes.text().catch(() => "<no-body>");
throw new Error(
`Failed fetching user: ${userRes.status} ${userRes.statusText} ${text}`
);
}
const userJson = await userRes.json(); const userJson = await userRes.json();
// Fetch guilds // Fetch guilds
@@ -757,6 +777,17 @@ export const server = createServer(
headers: { Authorization: `Bearer ${accessToken}` }, headers: { Authorization: `Bearer ${accessToken}` },
} }
); );
if (!guildsRes.ok) {
const text = await guildsRes.text().catch(() => "<no-body>");
console.warn(
"Failed fetching guilds for user; continuing with empty list",
{
status: guildsRes.status,
statusText: guildsRes.statusText,
body: text,
}
);
}
const guildsJson = guildsRes.ok ? await guildsRes.json() : []; const guildsJson = guildsRes.ok ? await guildsRes.json() : [];
// Filter guilds where user is owner or has ADMINISTRATOR bit // Filter guilds where user is owner or has ADMINISTRATOR bit
@@ -804,6 +835,10 @@ export const server = createServer(
expires_at: now + Number(tokenJson.expires_in || 3600) * 1000, expires_at: now + Number(tokenJson.expires_in || 3600) * 1000,
}); });
setSessionCookie(res, sid); setSessionCookie(res, sid);
// consume the state so it cannot be replayed
try {
STATE_STORE.delete(state);
} catch {}
res.writeHead( res.writeHead(
302, 302,
applySecurityHeadersForRequest(req, { Location: "/dashboard" }) applySecurityHeadersForRequest(req, { Location: "/dashboard" })