{"record":{"id":"f0bbcc4a2cda8ce5","repo":"decolua/9router","slug":"scope-is-required-for-external-idp-refresh","errorCode":null,"errorMessage":"scope is required for external_idp refresh","messagePattern":"scope is required for external_idp refresh","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/lib/oauth/kiroExternalIdp.js","lineNumber":137,"sourceCode":"      profileArn,\n      region,\n      authMethod: \"external_idp\",\n      provider: \"CLIProxyAPI\",\n      clientId,\n      tokenEndpoint,\n      scope,\n    },\n  };\n}\n\nexport function buildExternalIdpRefreshParams(refreshToken, providerSpecificData = {}) {\n  const clientId = normalizeString(providerSpecificData.clientId || providerSpecificData.client_id);\n  const tokenEndpoint = validateMicrosoftTokenEndpoint(providerSpecificData.tokenEndpoint || providerSpecificData.token_endpoint);\n  const scope = normalizeScope(providerSpecificData.scope || providerSpecificData.scopes);\n\n  if (!refreshToken) throw new Error(\"refresh token is required\");\n  if (!clientId) throw new Error(\"clientId is required for external_idp refresh\");\n  if (!scope) throw new Error(\"scope is required for external_idp refresh\");\n\n  return {\n    tokenEndpoint,\n    body: new URLSearchParams({\n      grant_type: \"refresh_token\",\n      client_id: clientId,\n      refresh_token: refreshToken,\n      scope,\n    }),\n    providerSpecificData: {\n      ...providerSpecificData,\n      authMethod: \"external_idp\",\n      clientId,\n      tokenEndpoint,\n      scope,\n    },\n  };\n}","sourceCodeStart":119,"sourceCodeEnd":155,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/kiroExternalIdp.js#L119-L155","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Re-import or re-save the Kiro external_idp account so providerSpecificData includes scope/scopes (normalizeKiroExternalIdpAuth enforces scopes on import).","Manually patch the account record: set providerSpecificData.scope to the original grant scope string (e.g. 'openid profile email offline_access').","Check that the code path populating providerSpecificData passes scope through (field names scope/scopes) rather than renaming it along the way.","If you control the source auth JSON, add the scopes field and retry the import."],"exampleFix":"// before: providerSpecificData stored without scope\nproviderSpecificData: { profileArn, region, authMethod: 'external_idp', clientId, tokenEndpoint }\n// after\nproviderSpecificData: { profileArn, region, authMethod: 'external_idp', clientId, tokenEndpoint, scope: 'openid profile email offline_access' }","handlingStrategy":"validation","validationCode":"const psd = account.providerSpecificData || {};\nconst scope = (Array.isArray(psd.scope || psd.scopes) ? (psd.scope || psd.scopes).join(' ') : (psd.scope || psd.scopes) || '').trim();\nif (!scope) throw new Error(`Kiro account ${account.id} has no stored scope; re-import the external_idp auth`);","typeGuard":"function hasExternalIdpRefreshData(psd) {\n  return Boolean(\n    psd && typeof psd === 'object' &&\n    typeof (psd.clientId ?? psd.client_id) === 'string' && (psd.clientId ?? psd.client_id).trim() &&\n    typeof (psd.scope ?? psd.scopes) !== 'undefined' && String(Array.isArray(psd.scope ?? psd.scopes) ? (psd.scope ?? psd.scopes).join(' ') : (psd.scope ?? psd.scopes)).trim()\n  );\n}","tryCatchPattern":"try {\n  const params = buildExternalIdpRefreshParams(refreshToken, providerSpecificData);\n  // ... refresh\n} catch (err) {\n  if (/scope is required for external_idp refresh/.test(err.message)) {\n    // mark account needs-reauth and surface a re-import action\n  } else throw err;\n}","preventionTips":["Always persist providerSpecificData.scope via normalizeKiroExternalIdpAuth when importing accounts.","Add a startup integrity check that flags external_idp accounts missing clientId/scope/tokenEndpoint.","Never hand-edit the accounts table; go through the importer which validates required fields.","Round-trip test: import an auth JSON, read it back, and assert scope survives serialization."],"tags":["oauth","kiro","missing-field","token-refresh"],"backgroundTag":"missing-oauth-scope","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}