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';