- 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.
97 lines
3.0 KiB
JavaScript
97 lines
3.0 KiB
JavaScript
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();
|
|
};
|
|
}
|