This commit is contained in:
Char committed 2026-05-19 23:22:11 +01:00
1 parent 9332fc9254
commit 7e22831d08
20 files changed
+945 -1489

No files matched your search

+133 -6
View File
@@ -3,7 +3,9 @@ const axios = require("axios");
const DISCORD_API = "https://discord.com/api";
const OAUTH_STATE_COOKIE = "sb_oauth_state";
const OAUTH_REDIRECT_COOKIE = "sb_oauth_redirect";
const OAUTH_STATE_MAX_AGE_MS = 10 * 60 * 1000;
const CALLBACK_PATH = "/api/auth/discord/callback";
function discordAvatarUrl(user) {
if (user.avatar) {
@@ -14,10 +16,125 @@ function discordAvatarUrl(user) {
return `https://cdn.discordapp.com/embed/avatars/${index}.png`;
}
function buildAuthorizeUrl(oauth, state) {
function getTrustProxySetting(apiConfig) {
if (apiConfig?.TrustProxy === false) return false;
if (typeof apiConfig?.TrustProxy === "number") return apiConfig.TrustProxy;
if (apiConfig?.TrustProxy === true) return 1;
return false;
}
function getRequestProto(req, apiConfig) {
if (apiConfig?.TrustProxy !== false) {
const forwarded = req.get("x-forwarded-proto");
if (forwarded) {
return forwarded.split(",")[0].trim().toLowerCase();
}
}
return (req.protocol || "http").toLowerCase();
}
function getRequestHost(req) {
const forwarded = req.get("x-forwarded-host");
if (forwarded) {
return forwarded.split(",")[0].trim();
}
return req.get("host") || "";
}
function getRequestOrigin(req, apiConfig) {
const proto = getRequestProto(req, apiConfig);
const host = getRequestHost(req);
if (!host) return null;
return `${proto}://${host}`;
}
function normalizeRedirectUri(uri) {
try {
const u = new URL(uri);
u.hash = "";
u.search = "";
return u.toString();
} catch {
return null;
}
}
function redirectUriMatchesHostPath(a, b) {
try {
const ua = new URL(a);
const ub = new URL(b);
return ua.host === ub.host && ua.pathname === ub.pathname;
} catch {
return false;
}
}
function collectConfiguredRedirectUris(oauth) {
const list = [];
if (oauth?.RedirectUri) {
const normalized = normalizeRedirectUri(oauth.RedirectUri);
if (normalized) list.push(normalized);
}
if (Array.isArray(oauth?.RedirectUris)) {
for (const entry of oauth.RedirectUris) {
const normalized = normalizeRedirectUri(entry);
if (normalized && !list.includes(normalized)) {
list.push(normalized);
}
}
}
return list;
}
/**
* Pick the OAuth redirect_uri for this request. When UseRequestOrigin is enabled
* (default), uses the same host/path as RedirectUri but the protocol the user
* actually used (http vs https). Register every variant in the Discord portal.
*/
function resolveOAuthRedirectUri(req, oauth, apiConfig) {
const configured = collectConfiguredRedirectUris(oauth);
const fallback = configured[0] || oauth?.RedirectUri || "";
const useRequestOrigin = oauth?.UseRequestOrigin !== false;
if (!useRequestOrigin || !req) {
return fallback;
}
const origin = getRequestOrigin(req, apiConfig);
if (!origin) return fallback;
const dynamic = `${origin}${CALLBACK_PATH}`;
const normalizedDynamic = normalizeRedirectUri(dynamic);
if (!normalizedDynamic) return fallback;
if (configured.includes(normalizedDynamic)) {
return normalizedDynamic;
}
const matchesConfigured = configured.some((uri) =>
redirectUriMatchesHostPath(uri, normalizedDynamic),
);
if (matchesConfigured) {
return normalizedDynamic;
}
return fallback;
}
function isSecureRequest(req, oauth, apiConfig) {
if (oauth?.CookieSecure === true) return true;
if (oauth?.CookieSecure === false) return false;
if (req) {
if (typeof req.secure === "boolean") return req.secure;
return getRequestProto(req, apiConfig) === "https";
}
return process.env.NODE_ENV === "production";
}
function buildAuthorizeUrl(oauth, state, redirectUri) {
const params = new URLSearchParams({
client_id: oauth.ClientId,
redirect_uri: oauth.RedirectUri,
redirect_uri: redirectUri,
response_type: "code",
scope: "identify",
state,
@@ -26,13 +143,13 @@ function buildAuthorizeUrl(oauth, state) {
return `${DISCORD_API}/oauth2/authorize?${params.toString()}`;
}
async function exchangeCode(oauth, code) {
async function exchangeCode(oauth, code, redirectUri) {
const body = new URLSearchParams({
client_id: oauth.ClientId,
client_secret: oauth.ClientSecret,
grant_type: "authorization_code",
code,
redirect_uri: oauth.RedirectUri,
redirect_uri: redirectUri,
});
const { data } = await axios.post(`${DISCORD_API}/oauth2/token`, body, {
@@ -55,16 +172,20 @@ function createOAuthState() {
return crypto.randomBytes(24).toString("hex");
}
function oauthStateCookieOptions() {
function oauthStateCookieOptions(req, oauth, apiConfig) {
return {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
secure: isSecureRequest(req, oauth, apiConfig),
sameSite: "lax",
maxAge: OAUTH_STATE_MAX_AGE_MS,
path: "/",
};
}
function oauthRedirectCookieOptions(req, oauth, apiConfig) {
return oauthStateCookieOptions(req, oauth, apiConfig);
}
function isOAuthConfigured(oauth) {
return Boolean(
oauth?.Enabled &&
@@ -93,12 +214,18 @@ function formatPublicUser(user) {
module.exports = {
OAUTH_STATE_COOKIE,
OAUTH_REDIRECT_COOKIE,
OAUTH_STATE_MAX_AGE_MS,
CALLBACK_PATH,
getTrustProxySetting,
resolveOAuthRedirectUri,
isSecureRequest,
buildAuthorizeUrl,
exchangeCode,
fetchDiscordUser,
createOAuthState,
oauthStateCookieOptions,
oauthRedirectCookieOptions,
isOAuthConfigured,
isUserAllowed,
formatPublicUser,