Budibase/budibase · error · Error

Microsoft OAuth response did not include a refresh token

Error message

Microsoft OAuth response did not include a refresh token

What it means

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.

Source

Thrown at packages/server/src/api/controllers/ai/sharepointAuth.ts:164

      redirect_uri: callbackUrl,
      scope: DEFAULT_SCOPE,
    }),
  })
  const tokenPayload = await tokenResponse.json()
  if (!tokenResponse.ok) {
    console.error("Microsoft OAuth token exchange failed", {
      appId,
      status: tokenResponse.status,
      error: tokenPayload?.error,
      hasDescription: !!tokenPayload?.error_description,
    })
    throw new Error("Failed to exchange Microsoft OAuth code")
  }

  const refreshToken = tokenPayload?.refresh_token
  const accessToken = tokenPayload?.access_token
  if (!refreshToken) {
    throw new Error("Microsoft OAuth response did not include a refresh token")
  }
  if (!accessToken) {
    throw new Error("Microsoft OAuth response did not include an access token")
  }

  const expiresIn = Number(tokenPayload?.expires_in || 0)
  const tokenType = tokenPayload?.token_type || "Bearer"
  const bearerToken = `${tokenType} ${accessToken}`
  let account = "unknown"

  try {
    const meResponse = await fetch(
      `${MICROSOFT_GRAPH_BASE}/me?$select=displayName,mail,userPrincipalName`,
      {
        headers: {
          Authorization: bearerToken,
        },
      }

View on GitHub (pinned to a81a902e9a)

Solutions

  1. Add offline_access to the OAuth authorization scope and have the user re-consent
  2. Verify the Azure AD app has the required delegated permissions (offline_access, Files.Read.All/Sites.Read.All) and admin consent granted
  3. Log tokenPayload keys (never values) to confirm which scopes were actually granted, then retry the code exchange

Example fix

// before
const authUrl = `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize?...&scope=Files.Read.All`
// after
const authUrl = `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize?...&scope=offline_access Files.Read.All`
Defensive patterns

Strategy: validation

Validate before calling

const scopes = new URLSearchParams(authUrl).get('scope') || ''
if (!scopes.split(' ').includes('offline_access')) throw new Error('offline_access scope required for refresh token')

Type guard

function hasRefreshToken(p: unknown): p is { refresh_token: string } {
  return typeof p === 'object' && p !== null && 'refresh_token' in p && typeof (p as { refresh_token: unknown }).refresh_token === 'string'
}

Try / catch

try {
  await completeSharePointAuth(params)
} catch (e) {
  if (e.message.includes('refresh token')) {
    // restart OAuth flow with offline_access scope
  }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of Budibase/budibase@a81a902e9a (2026-08-29). Data as JSON: /api/errors/b11267fc9b3fbdcc. Report an issue: GitHub.