Refactor authentication handling and improve API client security

- Updated OAuth endpoints for Challonge and Discord in platforms configuration.
- Implemented session and CSRF cookie initialization in main application entry.
- Enhanced Challonge API client to avoid sending sensitive API keys from the browser.
- Modified tournament querying to handle new state definitions and improved error handling.
- Updated UI components to reflect server-side storage of authentication tokens.
- Improved user experience in API Key Manager and Authentication Hub with clearer messaging.
- Refactored client credentials management to support asynchronous operations.
- Adjusted API client tests to validate new request configurations.
- Updated Vite configuration to support session and CSRF handling through proxies.
This commit is contained in:
2026-02-03 12:50:11 -05:00
parent 161b758a1b
commit 700c1cbbbe
39 changed files with 2434 additions and 999 deletions

View File

@@ -0,0 +1,96 @@
import crypto from 'node:crypto';
import {
COOKIE_NAMES,
generateToken,
getLegacyCsrfCookieOptions,
getLegacySidCookieOptions,
getSidCookieOptions
} from '../utils/cookie-options.js';
function signSid(sessionSecret, sid) {
return crypto
.createHmac('sha256', sessionSecret)
.update(sid)
.digest('base64url');
}
function parseAndVerifySignedSid(sessionSecret, signedValue) {
if (!signedValue || typeof signedValue !== 'string') return null;
const idx = signedValue.lastIndexOf('.');
if (idx <= 0) return null;
const sid = signedValue.slice(0, idx);
const sig = signedValue.slice(idx + 1);
if (!sid || !sig) return null;
const expected = signSid(sessionSecret, sid);
try {
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return null;
}
} catch {
return null;
}
return sid;
}
function getCookieValuesFromHeader(cookieHeader, name) {
if (!cookieHeader || typeof cookieHeader !== 'string') return [];
// Multiple cookies with the same name can exist if older cookies were scoped
// to a different path (e.g. '/api') than newer ones ('/').
const values = [];
const pattern = new RegExp(`(?:^|;\\s*)${name}=([^;]*)`, 'g');
let match;
while ((match = pattern.exec(cookieHeader)) !== null) {
values.push(match[1]);
}
return values;
}
export function sidMiddleware({ sessionSecret, config }) {
if (!sessionSecret) {
throw new Error('sidMiddleware requires sessionSecret');
}
return function sid(req, res, next) {
// If older cookies (scoped to '/api') exist alongside newer cookies
// (scoped to '/'), browsers may send both. Some parsers will then pick the
// "wrong" one depending on header order, causing auth to appear connected
// in one request and missing in another.
const rawCookieHeader = req.headers?.cookie || '';
if (rawCookieHeader.includes(`${COOKIE_NAMES.sid}=`)) {
res.clearCookie(COOKIE_NAMES.sid, getLegacySidCookieOptions(config));
}
if (rawCookieHeader.includes(`${COOKIE_NAMES.csrf}=`)) {
res.clearCookie(COOKIE_NAMES.csrf, getLegacyCsrfCookieOptions(config));
}
const signedCandidates = getCookieValuesFromHeader(
rawCookieHeader,
COOKIE_NAMES.sid
);
const signedFromParser = req.cookies?.[COOKIE_NAMES.sid];
if (signedFromParser) signedCandidates.push(signedFromParser);
// If multiple signed SIDs are present (legacy '/api' cookie + current '/'),
// browsers tend to send the more-specific path cookie first.
// Prefer the last valid SID to bias towards the newer '/' cookie.
let sid = null;
for (let i = signedCandidates.length - 1; i >= 0; i -= 1) {
const signed = signedCandidates[i];
sid = parseAndVerifySignedSid(sessionSecret, signed);
if (sid) break;
}
if (!sid) {
sid = generateToken(24);
const signedSid = `${sid}.${signSid(sessionSecret, sid)}`;
res.cookie(COOKIE_NAMES.sid, signedSid, getSidCookieOptions(config));
}
req.sid = sid;
next();
};
}