fix: show default oauth scopes in the admin dashboard #1126

This commit is contained in:
Linus Rath
2026-09-30 12:17:25 +02:00
parent 1eedfe5432
commit fee0e7c9e7
5 changed files with 16 additions and 7 deletions
+3 -1
View File
@@ -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
+2 -1
View File
@@ -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");
+3 -2
View File
@@ -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() {
<Text label="OAuth Client Secret" configKey="oauthClientSecret" value={currentValue('oauthClientSecret') as string} source={config.oauthClientSecret?.source} onChange={handleChange} onRevert={handleRevert} type="password" placeholder={config.oauthClientSecret?.hasValue ? '•••••••• (saved - type to replace)' : undefined} />
<Text label="OAuth Issuer URL" configKey="oauthIssuerUrl" value={currentValue('oauthIssuerUrl') as string} source={config.oauthIssuerUrl?.source} onChange={handleChange} onRevert={handleRevert} placeholder="https://auth.example.com" />
<Toggle label="Allow private OAuth endpoints" description="Permit discovered endpoints on other hosts to resolve to RFC-1918 / loopback addresses. Endpoints on the issuer's own host are always allowed, so split-DNS setups rarely need this." configKey="oauthAllowPrivateEndpoints" value={currentValue('oauthAllowPrivateEndpoints') as boolean} source={config.oauthAllowPrivateEndpoints?.source} onChange={handleChange} onRevert={handleRevert} />
<Text label="OAuth Scopes" description="Space-separated scopes that replace the defaults. Leave blank to use the built-in scope list." configKey="oauthScopes" value={currentValue('oauthScopes') as string} source={config.oauthScopes?.source} onChange={handleChange} onRevert={handleRevert} placeholder="openid email offline_access" />
<Text label="OAuth Extra Scopes" description="Additional space-separated scopes appended to the defaults." configKey="oauthExtraScopes" value={currentValue('oauthExtraScopes') as string} source={config.oauthExtraScopes?.source} onChange={handleChange} onRevert={handleRevert} placeholder="urn:ietf:params:oauth:..." />
<Text label="OAuth Scopes" description={`Space-separated scopes that replace the defaults (${DEFAULT_OAUTH_SCOPES}). Leave blank to use them. offline_access is not requested by default; add it here or under Extra Scopes if your provider only issues refresh tokens for it, or sessions end when the access token expires.`} configKey="oauthScopes" value={currentValue('oauthScopes') as string} source={config.oauthScopes?.source} onChange={handleChange} onRevert={handleRevert} placeholder={DEFAULT_OAUTH_SCOPES} />
<Text label="OAuth Extra Scopes" description={`Additional space-separated scopes appended to the defaults (${DEFAULT_OAUTH_SCOPES}). Ignored when OAuth Scopes is set.`} configKey="oauthExtraScopes" value={currentValue('oauthExtraScopes') as string} source={config.oauthExtraScopes?.source} onChange={handleChange} onRevert={handleRevert} placeholder="urn:ietf:params:oauth:..." />
</Section>
<Section title="Single Sign-On">
+6
View File
@@ -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';
+2 -3
View File
@@ -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<string>('oauthScopes', '');
if (explicit) return explicit;
const extra = configManager.get<string>('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';