{"record":{"id":"b11267fc9b3fbdcc","repo":"Budibase/budibase","slug":"microsoft-oauth-response-did-not-include-a-refresh","errorCode":null,"errorMessage":"Microsoft OAuth response did not include a refresh token","messagePattern":"Microsoft OAuth response did not include a refresh token","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/server/src/api/controllers/ai/sharepointAuth.ts","lineNumber":164,"sourceCode":"      redirect_uri: callbackUrl,\n      scope: DEFAULT_SCOPE,\n    }),\n  })\n  const tokenPayload = await tokenResponse.json()\n  if (!tokenResponse.ok) {\n    console.error(\"Microsoft OAuth token exchange failed\", {\n      appId,\n      status: tokenResponse.status,\n      error: tokenPayload?.error,\n      hasDescription: !!tokenPayload?.error_description,\n    })\n    throw new Error(\"Failed to exchange Microsoft OAuth code\")\n  }\n\n  const refreshToken = tokenPayload?.refresh_token\n  const accessToken = tokenPayload?.access_token\n  if (!refreshToken) {\n    throw new Error(\"Microsoft OAuth response did not include a refresh token\")\n  }\n  if (!accessToken) {\n    throw new Error(\"Microsoft OAuth response did not include an access token\")\n  }\n\n  const expiresIn = Number(tokenPayload?.expires_in || 0)\n  const tokenType = tokenPayload?.token_type || \"Bearer\"\n  const bearerToken = `${tokenType} ${accessToken}`\n  let account = \"unknown\"\n\n  try {\n    const meResponse = await fetch(\n      `${MICROSOFT_GRAPH_BASE}/me?$select=displayName,mail,userPrincipalName`,\n      {\n        headers: {\n          Authorization: bearerToken,\n        },\n      }","sourceCodeStart":146,"sourceCodeEnd":182,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/api/controllers/ai/sharepointAuth.ts#L146-L182","documentation":"completeSharePointAuth exchanges an OAuth authorization code with Microsoft's token endpoint. Microsoft requires the offline_access scope to return a refresh_token; this error means the token response parsed fine but had no refresh_token field, so SharePoint credentials cannot be stored for long-term use.","triggerScenarios":"Calling completeSharePointAuth with a valid code but the OAuth authorize URL omitted the offline_access scope (or the tenant admin revoked it), so Microsoft's token response lacks refresh_token.","commonSituations":"Constructing a custom consent URL without offline_access; admin consent flows that drop scopes; Microsoft sometimes omits refresh_token on first-party/implicit flows or when the app is configured for token-only auth.","solutions":["Add offline_access to the OAuth authorization scope and have the user re-consent","Verify the Azure AD app has the required delegated permissions (offline_access, Files.Read.All/Sites.Read.All) and admin consent granted","Log tokenPayload keys (never values) to confirm which scopes were actually granted, then retry the code exchange"],"exampleFix":"// before\nconst authUrl = `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize?...&scope=Files.Read.All`\n// after\nconst authUrl = `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize?...&scope=offline_access Files.Read.All`","handlingStrategy":"validation","validationCode":"const scopes = new URLSearchParams(authUrl).get('scope') || ''\nif (!scopes.split(' ').includes('offline_access')) throw new Error('offline_access scope required for refresh token')","typeGuard":"function hasRefreshToken(p: unknown): p is { refresh_token: string } {\n  return typeof p === 'object' && p !== null && 'refresh_token' in p && typeof (p as { refresh_token: unknown }).refresh_token === 'string'\n}","tryCatchPattern":"try {\n  await completeSharePointAuth(params)\n} catch (e) {\n  if (e.message.includes('refresh token')) {\n    // restart OAuth flow with offline_access scope\n  }\n}","preventionTips":["Always include offline_access in Microsoft OAuth scopes","Grant admin consent for all required delegated permissions in Azure AD","Capture and log granted scopes (not values) after token exchange to detect drift early"],"tags":["oauth","microsoft","sharepoint","missing-token"],"backgroundTag":"oauth-refresh-token-missing","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}