From 0a6f19ad1b8acfc8790bf956ca61811e9603a013 Mon Sep 17 00:00:00 2001 From: Musiker15 Date: Thu, 25 Jun 2026 12:14:47 +0200 Subject: [PATCH] fix(auth): use the guild OAuth app for header login on custom domains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a custom domain the header "Log in" button pointed at the absolute primary URL (appBaseUrl), so it always ran the GLOBAL MSK Forms OAuth app and showed the MSK Forms consent screen — even when the guild had its own Discord app configured. The public form login link was already relative and host-aware; only the header bypassed it. - Header login is now host-aware: on a custom domain whose guild has its own OAuth app (resolveHostOAuth != null) the login link is relative, so the host-aware login route uses the guild app and lands the session on the custom domain. Without an own app it still points to the primary host (state cookie / callback must be same-origin). Dashboard link unchanged (still primary-only). - Make the silent primary fallback diagnosable: log when a custom domain has no usable per-guild OAuth, and specifically when a stored secret is present but cannot be decrypted (the dashboard marks "Active" on presence alone). --- apps/web/src/app/api/auth/discord/login/route.ts | 6 +++++- apps/web/src/components/site-header.tsx | 16 ++++++++++------ apps/web/src/lib/guild-oauth.ts | 7 ++++++- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/apps/web/src/app/api/auth/discord/login/route.ts b/apps/web/src/app/api/auth/discord/login/route.ts index b55dc76..3e16c4a 100644 --- a/apps/web/src/app/api/auth/discord/login/route.ts +++ b/apps/web/src/app/api/auth/discord/login/route.ts @@ -30,7 +30,11 @@ export async function GET(request: NextRequest) { if (host && !isPrimaryHostname(host)) { const oauth = await resolveHostOAuth(host); if (!oauth) { - // No per-guild OAuth app — log in on the primary host instead. + // No usable per-guild OAuth app for this custom domain — fall back to the + // primary host. Logged so a mis-saved secret (present but undecryptable) + // or an unverified domain is diagnosable instead of silently bouncing to + // the global MSK Forms app. + console.warn(`[oauth-login] no per-guild OAuth for host "${host}" — falling back to primary host`); return NextResponse.redirect( absoluteUrl(`/api/auth/discord/login?returnTo=${encodeURIComponent(returnTo)}`), ); diff --git a/apps/web/src/components/site-header.tsx b/apps/web/src/components/site-header.tsx index c5b20f8..66ff3ea 100644 --- a/apps/web/src/components/site-header.tsx +++ b/apps/web/src/components/site-header.tsx @@ -6,6 +6,7 @@ import { ThemeToggle } from "@/components/theme-toggle"; import { Wordmark } from "@/components/landing/wordmark"; import { Button } from "@/components/ui/button"; import { isPrimaryHostname, requestHostname } from "@/lib/custom-domain"; +import { resolveHostOAuth } from "@/lib/guild-oauth"; import { appBaseUrl } from "@/lib/url"; import { getDict, getLocale } from "@/i18n"; @@ -18,14 +19,17 @@ export async function SiteHeader({ user }: { user: HeaderUser | null }) { const t = await getDict(); const locale = await getLocale(); - // Auth + dashboard live only on the primary domain (the OAuth state cookie and - // callback must be same-origin). On a guild's custom domain, point those links - // at the primary domain so login doesn't fail with a state mismatch. + // The dashboard lives only on the primary domain. Login is host-aware: on a + // custom domain whose guild has its OWN Discord OAuth app, log in right here + // (relative) so that app is used and the session lands on this domain. Without + // an own app, auth must run on the primary host (the OAuth state cookie and + // callback must be same-origin), else login fails with a state mismatch. const host = await requestHostname(); const onCustomDomain = Boolean(host) && !isPrimaryHostname(host!); - const base = onCustomDomain ? appBaseUrl() : ""; - const loginHref = `${base}/api/auth/discord/login`; - const dashboardHref = `${base}/dashboard`; + const ownOAuth = onCustomDomain ? await resolveHostOAuth(host!) : null; + const loginBase = onCustomDomain && !ownOAuth ? appBaseUrl() : ""; + const loginHref = `${loginBase}/api/auth/discord/login`; + const dashboardHref = `${onCustomDomain ? appBaseUrl() : ""}/dashboard`; return (
diff --git a/apps/web/src/lib/guild-oauth.ts b/apps/web/src/lib/guild-oauth.ts index a2f3963..2dd19fa 100644 --- a/apps/web/src/lib/guild-oauth.ts +++ b/apps/web/src/lib/guild-oauth.ts @@ -19,7 +19,12 @@ export async function getGuildOAuth(guildId: string): Promise }); if (!guild?.oauthClientId || !guild.oauthClientSecret) return null; const clientSecret = decryptSecret(guild.oauthClientSecret); - if (!clientSecret) return null; + if (!clientSecret) { + // Ciphertext present but undecryptable (e.g. SESSION_SECRET changed) — the + // dashboard shows "Active" on presence alone, so surface this here. + console.warn(`[guild-oauth] stored OAuth secret for guild ${guildId} could not be decrypted`); + return null; + } return { clientId: guild.oauthClientId, clientSecret }; }