From f39366b470de987bf77b70728edcc4587840bf61 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Fri, 15 May 2026 20:37:00 +0200 Subject: [PATCH] fix: read OAUTH_SCOPES at runtime instead of build time --- app/[locale]/login/page.tsx | 5 ++--- app/api/auth/sso/start/route.ts | 4 ++-- app/api/config/route.ts | 2 ++ hooks/use-config.ts | 4 ++++ lib/admin/types.ts | 2 ++ lib/oauth/tokens.ts | 18 ++++++++++++++++-- 6 files changed, 28 insertions(+), 7 deletions(-) diff --git a/app/[locale]/login/page.tsx b/app/[locale]/login/page.tsx index 2b974a63..458a4fc3 100644 --- a/app/[locale]/login/page.tsx +++ b/app/[locale]/login/page.tsx @@ -16,7 +16,6 @@ import { cn } from "@/lib/utils"; import { AlertCircle, Loader2, X, Info, Eye, EyeOff, LogIn, Sun, Moon, Monitor, Check, Shield, Play, Copy } from "lucide-react"; import { discoverOAuth, type OAuthMetadata } from "@/lib/oauth/discovery"; import { generateCodeVerifier, generateCodeChallenge, generateState } from "@/lib/oauth/pkce"; -import { OAUTH_SCOPES } from "@/lib/oauth/tokens"; import { useUpdateStore, selectBanner } from "@/stores/update-store"; import type { PublicJmapServerEntry } from "@/lib/admin/jmap-servers"; @@ -117,7 +116,7 @@ export default function LoginPage() { const isAddAccountMode = searchParams.get("mode") === "add-account"; const { login, loginDemo, isLoading, error, clearError, isAuthenticated } = useAuthStore(); const { theme, setTheme, initializeTheme } = useThemeStore(useShallow((s) => ({ theme: s.theme, setTheme: s.setTheme, initializeTheme: s.initializeTheme }))); - const { appName, jmapServerUrl: configuredServerUrl, oauthEnabled, oauthOnly, oauthClientId: globalOauthClientId, oauthIssuerUrl: globalOauthIssuerUrl, rememberMeEnabled, devMode, demoMode, loginLogoLightUrl, loginLogoDarkUrl, loginCompanyName, loginImprintUrl, loginPrivacyPolicyUrl, loginWebsiteUrl, isLoading: configLoading, error: configError, autoSsoEnabled, embeddedMode: _embeddedMode, allowCustomJmapEndpoint, jmapServers, jmapServerAutoPickByDomain } = useConfig(); + const { appName, jmapServerUrl: configuredServerUrl, oauthEnabled, oauthOnly, oauthClientId: globalOauthClientId, oauthIssuerUrl: globalOauthIssuerUrl, oauthScopes, rememberMeEnabled, devMode, demoMode, loginLogoLightUrl, loginLogoDarkUrl, loginCompanyName, loginImprintUrl, loginPrivacyPolicyUrl, loginWebsiteUrl, isLoading: configLoading, error: configError, autoSsoEnabled, embeddedMode: _embeddedMode, allowCustomJmapEndpoint, jmapServers, jmapServerAutoPickByDomain } = useConfig(); const resolvedTheme = useThemeStore((s) => s.resolvedTheme); const [formData, setFormData] = useState({ @@ -532,7 +531,7 @@ export default function LoginPage() { authUrl.searchParams.set("response_type", "code"); authUrl.searchParams.set("client_id", effectiveOauthClientId); authUrl.searchParams.set("redirect_uri", redirectUri); - authUrl.searchParams.set("scope", OAUTH_SCOPES); + authUrl.searchParams.set("scope", oauthScopes || "openid email profile"); authUrl.searchParams.set("state", state); authUrl.searchParams.set("code_challenge", challenge); authUrl.searchParams.set("code_challenge_method", "S256"); diff --git a/app/api/auth/sso/start/route.ts b/app/api/auth/sso/start/route.ts index dca051d1..85fce655 100644 --- a/app/api/auth/sso/start/route.ts +++ b/app/api/auth/sso/start/route.ts @@ -5,7 +5,7 @@ import { encryptPayload } from '@/lib/auth/crypto'; import { generateCodeVerifierServer, generateCodeChallengeServer, generateStateServer } from '@/lib/oauth/pkce-server'; import { getRequiredConfig } from '@/lib/oauth/token-exchange'; import { discoverOAuth } from '@/lib/oauth/discovery'; -import { OAUTH_SCOPES } from '@/lib/oauth/tokens'; +import { getOauthScopes } from '@/lib/oauth/tokens'; import { getCookieOptions } from '@/lib/oauth/cookie-config'; import { hasSessionSecret } from '@/lib/auth/session-secret'; @@ -73,7 +73,7 @@ export async function POST(request: NextRequest) { authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set('client_id', clientId); authUrl.searchParams.set('redirect_uri', redirect_uri); - authUrl.searchParams.set('scope', OAUTH_SCOPES); + authUrl.searchParams.set('scope', getOauthScopes()); authUrl.searchParams.set('state', state); authUrl.searchParams.set('code_challenge', codeChallenge); authUrl.searchParams.set('code_challenge_method', 'S256'); diff --git a/app/api/config/route.ts b/app/api/config/route.ts index 2640c2be..bef6b80a 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -3,6 +3,7 @@ import { logger } from '@/lib/logger'; import { configManager } from '@/lib/admin/config-manager'; import { parseJmapServers, redactJmapServers } from '@/lib/admin/jmap-servers'; import { hasSessionSecret } from '@/lib/auth/session-secret'; +import { getOauthScopes } from '@/lib/oauth/tokens'; /** * Runtime configuration endpoint @@ -35,6 +36,7 @@ export async function GET() { oauthOnly, oauthClientId: configManager.get('oauthClientId', ''), oauthIssuerUrl: configManager.get('oauthIssuerUrl', ''), + oauthScopes: getOauthScopes(), rememberMeEnabled: hasSessionSecret(), settingsSyncEnabled: configManager.get('settingsSyncEnabled', false) && hasSessionSecret(), stalwartFeaturesEnabled, diff --git a/hooks/use-config.ts b/hooks/use-config.ts index 935c29a6..db30f363 100644 --- a/hooks/use-config.ts +++ b/hooks/use-config.ts @@ -12,6 +12,7 @@ interface ConfigData { oauthOnly: boolean; oauthClientId: string; oauthIssuerUrl: string; + oauthScopes: string; rememberMeEnabled: boolean; settingsSyncEnabled: boolean; stalwartFeaturesEnabled: boolean; @@ -90,6 +91,7 @@ export function useConfig(): AppConfig { oauthOnly: configCache?.oauthOnly || false, oauthClientId: configCache?.oauthClientId || '', oauthIssuerUrl: configCache?.oauthIssuerUrl || '', + oauthScopes: configCache?.oauthScopes || '', rememberMeEnabled: configCache?.rememberMeEnabled || false, settingsSyncEnabled: configCache?.settingsSyncEnabled || false, stalwartFeaturesEnabled: configCache?.stalwartFeaturesEnabled ?? true, @@ -124,6 +126,7 @@ export function useConfig(): AppConfig { oauthOnly: configCache.oauthOnly, oauthClientId: configCache.oauthClientId, oauthIssuerUrl: configCache.oauthIssuerUrl, + oauthScopes: configCache.oauthScopes, rememberMeEnabled: configCache.rememberMeEnabled, settingsSyncEnabled: configCache.settingsSyncEnabled, stalwartFeaturesEnabled: configCache.stalwartFeaturesEnabled, @@ -159,6 +162,7 @@ export function useConfig(): AppConfig { oauthOnly: data.oauthOnly, oauthClientId: data.oauthClientId, oauthIssuerUrl: data.oauthIssuerUrl, + oauthScopes: data.oauthScopes, rememberMeEnabled: data.rememberMeEnabled, settingsSyncEnabled: data.settingsSyncEnabled, stalwartFeaturesEnabled: data.stalwartFeaturesEnabled, diff --git a/lib/admin/types.ts b/lib/admin/types.ts index 66ecf08d..98f994a8 100644 --- a/lib/admin/types.ts +++ b/lib/admin/types.ts @@ -146,6 +146,8 @@ export const CONFIG_ENV_MAP: Record('oauthScopes', ''); + if (explicit) return explicit; + const extra = configManager.get('oauthExtraScopes', ''); + return extra ? `${DEFAULT_SCOPES} ${extra}`.trim() : DEFAULT_SCOPES; +} export const REFRESH_TOKEN_COOKIE = 'jmap_rt'; export const REFRESH_TOKEN_SERVER_COOKIE = 'jmap_rts';