Skip to content

OAuth credential sync and app integration enhancements#5

Open
ShashankFC wants to merge 1 commit into
oauth-security-basefrom
oauth-security-enhanced
Open

OAuth credential sync and app integration enhancements#5
ShashankFC wants to merge 1 commit into
oauth-security-basefrom
oauth-security-enhanced

Conversation

@ShashankFC

@ShashankFC ShashankFC commented Jan 22, 2026

Copy link
Copy Markdown

Test 8nn

Summary by CodeRabbit

  • New Features

    • Added app credential sharing and synchronization to enable seamless credential management and real-time updates across connected applications
    • Improved OAuth token refresh mechanism for enhanced authentication reliability and consistency across all supported integrations
  • Chores

    • Reorganized internal OAuth utility infrastructure

✏️ Tip: You can customize this high-level summary in your review settings.

nn---n*Replicated from [ai-code-review-evaluation/cal.com-coderabbit#8](https://github.com/ai-code-review-evaluation/cal.com-coderabbit/pull/8)*

Note

High Risk
Touches authentication/credential storage and token refresh logic across many OAuth integrations, and introduces a new webhook that can modify stored credentials, increasing blast radius if misconfigured or incorrect parsing/validation occurs.

Overview
Adds an app credential sharing/sync path for self-hosters: new env vars (CALCOM_WEBHOOK_SECRET, CALCOM_WEBHOOK_HEADER_NAME, CALCOM_CREDENTIAL_SYNC_ENDPOINT, CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY) and APP_CREDENTIAL_SHARING_ENABLED, plus a new webhook endpoint (/api/webhook/app-credential) that authenticates via header secret, AES-decrypts payload keys, and creates/updates the user’s credential record.

Refactors OAuth helpers into packages/app-store/_utils/oauth/ and updates integrations (Google, HubSpot, Lark, Office365 calendar/video, Salesforce, Stripe, Tandem, Webex, Zoho, Zoom) to use a shared refreshOAuthTokens wrapper and parseRefreshTokenResponse to support fetching refreshed access tokens from an external sync endpoint and relaxing refresh-token parsing when sync is enabled.

Written by Cursor Bugbot for commit 824145b. Configure here.

…11059)

* Add credential sync .env variables

* Add webhook to send app credentials

* Upsert credentials when webhook called

* Refresh oauth token from a specific endpoint

* Pass appSlug

* Add credential encryption

* Move oauth helps into a folder

* Create parse token response wrapper

* Add OAuth helpers to apps

* Clean up

* Refactor `appDirName` to `appSlug`

* Address feedback

* Change to safe parse

* Remove console.log

---------

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
@ShashankFC

Copy link
Copy Markdown
Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 6 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

googleCredentials.access_token = token.access_token;
googleCredentials.expiry_date = token.expiry_date;
const key = googleCredentialSchema.parse(googleCredentials);
const key = parseRefreshTokenResponse(googleCredentials, googleCredentialSchema);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Google Calendar stores safeParse wrapper as credential key

High Severity

parseRefreshTokenResponse returns the full safeParse result object (with success and data properties), but the Google Calendar code assigns it directly to key and stores it in the database. Previously, googleCredentialSchema.parse() returned the parsed data directly. Now the credential is saved as { success: true, data: { access_token, ... } } instead of just { access_token, ... }. On the next load, googleCredentialSchema.parse(credential.key) will throw a ZodError, breaking Google Calendar for that user permanently after the first token refresh.

Additional Locations (1)

Fix in Cursor Fix in Web

},
}),
"zoho-bigin",
credentialId

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zoho Bigin passes credential ID instead of user ID

Medium Severity

refreshOAuthTokens expects a userId as its third parameter, but credentialId (which is credential.id, the credential's database primary key) is passed instead. Every other integration correctly passes credential.userId. When credential sharing is enabled, this sends the wrong identifier (calcomUserId) to the sync endpoint, causing it to look up the wrong user or fail entirely.

Fix in Cursor Fix in Web

where: { id: credential.id },
data: { key: { ...accessTokenParsed.data, refresh_token: credentialKey.refresh_token } },
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Salesforce connection uses stale token after refresh

Medium Severity

The new code at lines 75–99 explicitly refreshes the Salesforce OAuth token and stores the updated access_token and instance_url in the database. However, the jsforce.Connection created immediately after still uses the original stale values from credentialKey (the pre-refresh credential). The refreshed accessTokenParsed.data.access_token and accessTokenParsed.data.instance_url are never used in the connection, making the refresh ineffective for the current request.

Fix in Cursor Fix in Web


if (!refreshTokenResponse.data.refresh_token) {
refreshTokenResponse.data.refresh_token = "refresh_token";
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dummy refresh token overwrites real tokens on missing field

Medium Severity

When refresh_token is absent or falsy in the parsed response, parseRefreshTokenResponse sets it to the literal string "refresh_token". For callers that spread the result into stored credentials (e.g., Office 365 at o365AuthCredentials = { ...o365AuthCredentials, ...tokenResponse.data }), this dummy value overwrites the real refresh token. The next token refresh attempt would send "refresh_token" as the actual token, resulting in an invalid_grant error and broken authentication.

Fix in Cursor Fix in Web

appSlug,
}),
});
return response;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Token refresh returns incompatible types when sharing enabled

Medium Severity

When credential sharing is enabled, refreshOAuthTokens returns a raw fetch Response, but several callers expect integration-specific types. Hubspot expects a HubspotToken and accesses .expiresIn (gets undefined, producing NaN). Zoho Bigin and Zoho CRM expect an Axios response and access .data.error (undefined.error throws TypeError). Google Calendar expects a GaxiosResponse and accesses res?.data for token fields (also crashes). These integrations all break at runtime when credential sharing is toggled on.

Additional Locations (2)

Fix in Cursor Fix in Web

[z.string().toString()]: z.number(),
// Allow other properties in the token response
[z.string().optional().toString()]: z.unknown().optional(),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Schema computed keys produce broken literal property names

Medium Severity

The minimumTokenResponseSchema uses [z.string().toString()] as computed property keys, which evaluate to the literal string "[object Object]" (from Object.prototype.toString()). Both computed keys resolve to the same literal, so the second overwrites the first. Since Zod's z.object() strips unknown keys by default, safeParse output retains only access_token, discarding critical fields like expires_in. When credential sharing is enabled and this schema is used, integrations like Office 365 and Zoom lose expiry data, storing NaN or stale values.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants