{"record":{"id":"489a9d360a934ce1","repo":"paperclipai/paperclip","slug":"connector-reauthorization-required","errorCode":"connector_reauthorization_required","errorMessage":"Legacy authorization must be reconnected through Paperclip Cloud","messagePattern":"Legacy authorization must be reconnected through Paperclip Cloud","errorType":"http","errorClass":"ToolGatewayHttpError","httpStatus":409,"severity":"error","filePath":"server/src/services/tool-gateway.ts","lineNumber":2783,"sourceCode":"      ? Date.parse(grantOauth.accessTokenExpiresAt)\n      : Number.NaN;\n    const currentTime = options.now?.() ?? Date.now();\n    // The preferred GitHub App policy yields a non-expiring ghu_ token and no\n    // refresh token. Absence of an expiry is deliberate, not an invitation to\n    // enter the rotation path.\n    if (grantOauth?.accessTokenExpiresAt === null || grantOauth?.accessTokenExpiresAt === undefined) return grant;\n    const refreshedAt = typeof grantOauth.refreshedAt === \"string\" ? Date.parse(grantOauth.refreshedAt) : Number.NaN;\n    const rotationDue = !Number.isFinite(refreshedAt) || refreshedAt <= currentTime - 30 * 24 * 60 * 60_000;\n    if (!forceRefresh && Number.isFinite(expiresAt) && expiresAt > currentTime + 60 * 60_000 && !rotationDue) return grant;\n    if (oauth.strategy === \"paperclip_id_connector\") {\n      // Paperclip ID used different endpoints, signing metadata, envelope\n      // purposes, and a different Google client. Its refresh token cannot be\n      // exchanged through Paperclip Cloud. Let an unexpired access token finish\n      // its useful life, then require an explicit managed-connector enrollment\n      // and provider reconnect instead of sending it to the wrong client.\n      await db.update(connectionGrants).set({ status: \"needs_reauthorization\", updatedAt: new Date(currentTime) })\n        .where(eq(connectionGrants.id, grant.id));\n      throw new ToolGatewayHttpError(409, \"Legacy authorization must be reconnected through Paperclip Cloud\", \"connector_reauthorization_required\", {\n        connectionId: connection.id,\n        grantId: grant.id,\n      });\n    }\n    const existingFlight = gmailRefreshFlights.get(grant.id);\n    if (existingFlight) return existingFlight;\n    const refresh = (async () => {\n      const cloudConnector = currentCloudConnector();\n      if (!cloudConnector || !connectorSubject) {\n        await db.update(connectionGrants).set({ status: \"needs_reauthorization\", updatedAt: new Date(currentTime) })\n          .where(eq(connectionGrants.id, grant.id));\n        throw new ToolGatewayHttpError(409, \"Managed authorization must be reconnected\", \"connector_reauthorization_required\", {\n          connectionId: connection.id,\n          grantId: grant.id,\n        });\n      }\n      const accessRef = grant.credentialSecretRefs.find((ref) => ref.configPath === \"oauth.access_token\");\n      const refreshRef = grant.credentialSecretRefs.find((ref) => ref.configPath === \"oauth.refresh_token\");","sourceCodeStart":2765,"sourceCodeEnd":2801,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/tool-gateway.ts#L2765-L2801","documentation":"Thrown by the tool gateway when a Gmail/Google connector grant was created under the legacy (unmanaged) OAuth client. Its refresh token cannot be exchanged through Paperclip Cloud, so the gateway marks the grant needs_reauthorization and refuses to refresh it, telling the caller to re-enroll the connector as a managed connector and reconnect.","triggerScenarios":"Any managed token refresh attempt (refreshManagedGmailGrant path) on a grant whose credential refs carry the legacy client identity: the refresh token is from a different Google client than the current Paperclip Cloud connector, so exchanging it is rejected.","commonSituations":"An instance migrated from self-hosted/legacy Google OAuth setup to Paperclip Cloud managed connectors; a Google app re-configuration changed the client ID behind existing stored grants; grants created before managed-connector enrollment are still in use after their access token expired.","solutions":["Reconnect the connection through Paperclip Cloud (managed connector enrollment flow) to obtain a fresh refresh token for the current client","Re-enroll the connector as a managed connector if it is still flagged as legacy","Until reconnected, rely on the unexpired access token for short-lived operations","Re-create the connection from scratch if reconnect is unavailable"],"exampleFix":"// before\nconst grant = await getGrant(connectionId); // legacy grant, refresh throws 409\nawait gateway.refresh(connectionId);\n// after\nif (grant.isLegacy) {\n  await reconnectViaPaperclipCloud(connectionId); // managed-connector enrollment\n}\nawait gateway.refresh(connectionId);","handlingStrategy":"try-catch","validationCode":"const grant = await getGrant(connId);\nif (grant.status === \"needs_reauthorization\" || grant.isLegacy) await reconnectViaPaperclipCloud(connId);","typeGuard":"function isReauthRequired(e: unknown): e is ToolGatewayHttpError {\n  return e instanceof ToolGatewayHttpError && e.code === \"connector_reauthorization_required\";\n}","tryCatchPattern":"try { await gateway.callTool(session, connId, p); }\ncatch (e) {\n  if (e instanceof ToolGatewayHttpError && e.code === \"connector_reauthorization_required\") {\n    await reconnectManagedConnection(connId);\n  } else throw e;\n}","preventionTips":["Migrate legacy OAuth grants to managed connectors proactively after enabling Paperclip Cloud","Monitor grants with status needs_reauthorization and alert","Track legacy client identifiers in grants and flag them before access tokens expire","Re-connect connections after any Google client/app reconfiguration"],"tags":["oauth","connector","google","reauthorization"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}