diff --git a/.env.example b/.env.example index 14a59e603..b6767333b 100644 --- a/.env.example +++ b/.env.example @@ -88,7 +88,9 @@ JMAP_SERVER_URL=https://your-jmap-server.com # OAUTH_POST_LOGOUT_REDIRECT_URI=https://mail.example.com/en/login # Replace the scopes requested at authorization. Space-separated. Leave unset -# to use the defaults the client already asks for. +# to use the defaults: openid email profile. offline_access is not among them; +# add it if your IdP (e.g. Authelia) only issues refresh tokens for it, +# otherwise sessions end when the access token expires. # OAUTH_SCOPES=openid email profile offline_access # Append scopes instead of replacing them. Use this when your IdP needs one diff --git a/app/(main)/[locale]/login/page.tsx b/app/(main)/[locale]/login/page.tsx index 17a6bbd8b..80958818a 100644 --- a/app/(main)/[locale]/login/page.tsx +++ b/app/(main)/[locale]/login/page.tsx @@ -17,6 +17,7 @@ import { cn } from "@/lib/utils"; import { AlertCircle, Loader2, X, Info, Eye, EyeOff, LogIn, Sun, Moon, Monitor, Check, Shield, Play, Copy } from "@/components/icons"; import { type OAuthMetadata } from "@/lib/oauth/discovery"; import { generateCodeVerifier, generateCodeChallenge, generateState } from "@/lib/oauth/pkce"; +import { DEFAULT_OAUTH_SCOPES } from "@/lib/oauth/scopes"; import { useUpdateStore, selectBanner } from "@/stores/update-store"; import type { PublicJmapServerEntry } from "@/lib/admin/jmap-servers"; import { IS_LITE, getLiteInjectedClientId } from "@/lib/lite"; @@ -758,7 +759,7 @@ function LoginPageContent() { authUrl.searchParams.set("response_type", "code"); authUrl.searchParams.set("client_id", effectiveOauthClientId); authUrl.searchParams.set("redirect_uri", redirectUri); - authUrl.searchParams.set("scope", oauthScopes || "openid email profile"); + authUrl.searchParams.set("scope", oauthScopes || DEFAULT_OAUTH_SCOPES); authUrl.searchParams.set("state", state); authUrl.searchParams.set("code_challenge", challenge); authUrl.searchParams.set("code_challenge_method", "S256"); diff --git a/app/(main)/admin/_tabs/auth.tsx b/app/(main)/admin/_tabs/auth.tsx index 7034407a1..2b3b977b1 100644 --- a/app/(main)/admin/_tabs/auth.tsx +++ b/app/(main)/admin/_tabs/auth.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'; import { Save, Loader2, RotateCcw, Sparkles } from '@/components/icons'; import { apiFetch } from '@/lib/browser-navigation'; +import { DEFAULT_OAUTH_SCOPES } from '@/lib/oauth/scopes'; interface ConfigEntry { // Sensitive keys (sessionSecret, oauthClientSecret) come back with @@ -274,8 +275,8 @@ export function AuthTab() { - - + +
diff --git a/lib/oauth/scopes.ts b/lib/oauth/scopes.ts new file mode 100644 index 000000000..e9126422e --- /dev/null +++ b/lib/oauth/scopes.ts @@ -0,0 +1,6 @@ +/** + * Scopes requested when neither OAUTH_SCOPES nor an admin override is set. + * Kept free of server imports so the login page and admin dashboard can show + * the same list the server actually sends. + */ +export const DEFAULT_OAUTH_SCOPES = 'openid email profile'; diff --git a/lib/oauth/tokens.ts b/lib/oauth/tokens.ts index 9d0719945..2661aaad1 100644 --- a/lib/oauth/tokens.ts +++ b/lib/oauth/tokens.ts @@ -1,6 +1,5 @@ import { configManager } from '@/lib/admin/config-manager'; - -const DEFAULT_SCOPES = 'openid email profile'; +import { DEFAULT_OAUTH_SCOPES } from './scopes'; /** * Resolve the OAuth scopes to request at authorize time. @@ -13,7 +12,7 @@ export function getOauthScopes(): string { const explicit = configManager.get('oauthScopes', ''); if (explicit) return explicit; const extra = configManager.get('oauthExtraScopes', ''); - return extra ? `${DEFAULT_SCOPES} ${extra}`.trim() : DEFAULT_SCOPES; + return extra ? `${DEFAULT_OAUTH_SCOPES} ${extra}`.trim() : DEFAULT_OAUTH_SCOPES; } export const REFRESH_TOKEN_COOKIE = 'jmap_rt'; export const REFRESH_TOKEN_SERVER_COOKIE = 'jmap_rts';