feat: hardcode Logto org scopes in auth flow, hide from admin UI
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m48s
CI / docker (push) Successful in 1m24s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 39s

Always include urn:logto:scope:organizations and
urn:logto:scope:organization_roles in OIDC auth requests. These are
required for role mapping in multi-tenant setups and harmless for
non-Logto providers (unknown scopes ignored per OIDC spec).

Filter them from the OIDC admin config page so they don't confuse
standalone server admins or SaaS tenants.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-10 15:37:40 +02:00
parent 0d610be3dc
commit f0658cbd07
3 changed files with 12 additions and 4 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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));
}, []);