decolua/9router · error

scope is required for external_idp refresh

Error message

scope is required for external_idp refresh

What it means

buildExternalIdpRefreshParams in src/lib/oauth/kiroExternalIdp.js builds the OAuth refresh_token request body for Kiro accounts that authenticate through an external Microsoft identity provider. Before issuing the refresh it validates that the stored providerSpecificData still contains clientId, a Microsoft-hosted tokenEndpoint, and a non-empty scope. This error is thrown when the scope (normalized from the `scope` or `scopes` field, array or string) resolves to an empty string, so the refresh body would be sent without a scope grant.

Source

Thrown at src/lib/oauth/kiroExternalIdp.js:137

      profileArn,
      region,
      authMethod: "external_idp",
      provider: "CLIProxyAPI",
      clientId,
      tokenEndpoint,
      scope,
    },
  };
}

export function buildExternalIdpRefreshParams(refreshToken, providerSpecificData = {}) {
  const clientId = normalizeString(providerSpecificData.clientId || providerSpecificData.client_id);
  const tokenEndpoint = validateMicrosoftTokenEndpoint(providerSpecificData.tokenEndpoint || providerSpecificData.token_endpoint);
  const scope = normalizeScope(providerSpecificData.scope || providerSpecificData.scopes);

  if (!refreshToken) throw new Error("refresh token is required");
  if (!clientId) throw new Error("clientId is required for external_idp refresh");
  if (!scope) throw new Error("scope is required for external_idp refresh");

  return {
    tokenEndpoint,
    body: new URLSearchParams({
      grant_type: "refresh_token",
      client_id: clientId,
      refresh_token: refreshToken,
      scope,
    }),
    providerSpecificData: {
      ...providerSpecificData,
      authMethod: "external_idp",
      clientId,
      tokenEndpoint,
      scope,
    },
  };
}

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Re-import or re-save the Kiro external_idp account so providerSpecificData includes scope/scopes (normalizeKiroExternalIdpAuth enforces scopes on import).
  2. Manually patch the account record: set providerSpecificData.scope to the original grant scope string (e.g. 'openid profile email offline_access').
  3. Check that the code path populating providerSpecificData passes scope through (field names scope/scopes) rather than renaming it along the way.
  4. If you control the source auth JSON, add the scopes field and retry the import.

Example fix

// before: providerSpecificData stored without scope
providerSpecificData: { profileArn, region, authMethod: 'external_idp', clientId, tokenEndpoint }
// after
providerSpecificData: { profileArn, region, authMethod: 'external_idp', clientId, tokenEndpoint, scope: 'openid profile email offline_access' }
Defensive patterns

Strategy: validation

Validate before calling

const psd = account.providerSpecificData || {};
const scope = (Array.isArray(psd.scope || psd.scopes) ? (psd.scope || psd.scopes).join(' ') : (psd.scope || psd.scopes) || '').trim();
if (!scope) throw new Error(`Kiro account ${account.id} has no stored scope; re-import the external_idp auth`);

Type guard

function hasExternalIdpRefreshData(psd) {
  return Boolean(
    psd && typeof psd === 'object' &&
    typeof (psd.clientId ?? psd.client_id) === 'string' && (psd.clientId ?? psd.client_id).trim() &&
    typeof (psd.scope ?? psd.scopes) !== 'undefined' && String(Array.isArray(psd.scope ?? psd.scopes) ? (psd.scope ?? psd.scopes).join(' ') : (psd.scope ?? psd.scopes)).trim()
  );
}

Try / catch

try {
  const params = buildExternalIdpRefreshParams(refreshToken, providerSpecificData);
  // ... refresh
} catch (err) {
  if (/scope is required for external_idp refresh/.test(err.message)) {
    // mark account needs-reauth and surface a re-import action
  } else throw err;
}

Prevention

When it happens

Trigger: refreshKiroToken calls buildExternalIdpRefreshParams with providerSpecificData that lacks both `scope` and `scopes` keys, or where the value is an empty string, an empty array, or an array of only whitespace strings.

Common situations: Kiro account records imported before scopes were persisted (older importer versions), records hand-edited in the SQLite accounts table with scope dropped, auth JSON pasted from CLIProxyAPI that omits the `scopes` field, or data round-tripped through a serializer that drops empty fields.

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/f0bbcc4a2cda8ce5. Report an issue: GitHub.