diff --git a/ui/src/auth/LoginPage.tsx b/ui/src/auth/LoginPage.tsx index 18d2cfff..ccaed52f 100644 --- a/ui/src/auth/LoginPage.tsx +++ b/ui/src/auth/LoginPage.tsx @@ -14,6 +14,10 @@ interface OidcInfo { additionalScopes?: string[]; } +// Logto org scopes required for role mapping in multi-tenant setups. +// Always requested, harmless for non-Logto providers (unknown scopes are ignored per OIDC spec). +const PLATFORM_SCOPES = ['urn:logto:scope:organizations', 'urn:logto:scope:organization_roles']; + const SUBTITLES = [ "Prove you're not a mirage", "Only authorized cameleers beyond this dune", @@ -76,7 +80,7 @@ export function LoginPage() { if (oidc && !forceLocal && !autoRedirected.current) { autoRedirected.current = true; const redirectUri = `${window.location.origin}${config.basePath}oidc/callback`; - const scopes = ['openid', 'email', 'profile', ...(oidc.additionalScopes || [])]; + const scopes = ['openid', 'email', 'profile', ...PLATFORM_SCOPES, ...(oidc.additionalScopes || [])]; const params = new URLSearchParams({ response_type: 'code', client_id: oidc.clientId, @@ -100,7 +104,7 @@ export function LoginPage() { if (!oidc) return; setOidcLoading(true); const redirectUri = `${window.location.origin}${config.basePath}oidc/callback`; - const scopes = ['openid', 'email', 'profile', ...(oidc.additionalScopes || [])]; + const scopes = ['openid', 'email', 'profile', ...PLATFORM_SCOPES, ...(oidc.additionalScopes || [])]; const params = new URLSearchParams({ response_type: 'code', client_id: oidc.clientId, diff --git a/ui/src/auth/OidcCallback.tsx b/ui/src/auth/OidcCallback.tsx index 9cca67bb..b1676002 100644 --- a/ui/src/auth/OidcCallback.tsx +++ b/ui/src/auth/OidcCallback.tsx @@ -31,7 +31,8 @@ export function OidcCallback() { api.GET('/auth/oidc/config').then(({ data }) => { if (data?.authorizationEndpoint && data?.clientId) { const redirectUri = `${window.location.origin}${config.basePath}oidc/callback`; - const scopes = ['openid', 'email', 'profile', ...(data.additionalScopes || [])]; + const PLATFORM_SCOPES = ['urn:logto:scope:organizations', 'urn:logto:scope:organization_roles']; + const scopes = ['openid', 'email', 'profile', ...PLATFORM_SCOPES, ...(data.additionalScopes || [])]; const p = new URLSearchParams({ response_type: 'code', client_id: data.clientId, diff --git a/ui/src/pages/Admin/OidcConfigPage.tsx b/ui/src/pages/Admin/OidcConfigPage.tsx index c630a787..e871e812 100644 --- a/ui/src/pages/Admin/OidcConfigPage.tsx +++ b/ui/src/pages/Admin/OidcConfigPage.tsx @@ -23,6 +23,9 @@ interface OidcFormData { additionalScopes: string[]; } +// Platform-managed scopes — always requested by the auth flow, hidden from the admin UI +const PLATFORM_SCOPES = new Set(['urn:logto:scope:organizations', 'urn:logto:scope:organization_roles']); + const EMPTY_CONFIG: OidcFormData = { enabled: false, autoSignup: true, @@ -61,7 +64,7 @@ export default function OidcConfigPage() { userIdClaim: data.userIdClaim ?? 'sub', defaultRoles: data.defaultRoles ?? ['VIEWER'], audience: (data as any).audience ?? '', - additionalScopes: (data as any).additionalScopes ?? [], + additionalScopes: ((data as any).additionalScopes ?? []).filter((s: string) => !PLATFORM_SCOPES.has(s)), })) .catch(() => setForm(EMPTY_CONFIG)); }, []);